問題描述

我在 header.php 中有一個變量,如:

$page_extra_title = get_post_meta($this_page->ID, "_theme_extra_title", true);

一旦我做:

var_dump($page_extra_title);

我總是在 header.php 之外得到 NULL(var_dump 僅在 header.php 中正常工作) 。我一直在粘貼同樣的變量,我需要它 (page.php,post.php,footer.php 等),但它是瘋狂,使一切幾乎不可能維護。

我想知道什麼是傳遞變量通過我的主題中的所有文件的最佳方法?我猜使用 functions.php 和”get_post_meta” 可能不是最好的主意? 🙂

最佳解決方案

基本分離的數據結構

要傳遞數據,您通常會使用一個 Model(“M” 在”MVC” 中) 。我們來看一個非常簡單的數據界面。接口僅用作我們的構建塊的”Recipes”:

namespace WeCodeMorePackageModels;
interface ArgsInterface
{
    public function getID();
    public function getLabel();
}

以上是我們傳遞的:一個常見的 ID 和一個”Label” 。

通過組合原子片顯示數據

接下來,我們需要一些 View,在我們的模型和我們的模板之間進行協商。

namespace WeCodeMorePackage;
interface PackageViewInterface
{
    /**
     * @param ModelsArgsInterface $args
     * @return int|void
     */
    public function render( ModelsArgsInterface $args );
}

基本上接口説

“We can render something and a Model is mandatory for that task”

最後我們需要實現上面的內容並構建實際的 View 。您可以看到,構造函數告訴我們,視圖的強制性是一個 Template,我們可以渲染它。為了方便開發,我們甚至檢查模板文件是否真實存在,這樣我們可以使其他開發人員的生活 (以及我們的) 更容易,並注意到這一點。

在 render 函數的第二步中,我們使用 Closure 構建實際的模板包裝器和 bindTo()模型的模板。

namespace WeCodeMorePackage;

use WeCodeMorePackageModelsArgsInterface;

/** @noinspection PhpInconsistentReturnPointsInspection */
class PackageView implements PackageViewInterface
{
    /** @var string|WP_Error */
    private $template;
    /**
     * @param string $template
     */
    public function __construct( $template )
    {
        $this->template = ! file_exists( $template )
            ? new WP_Error( 'wcm-package', 'A package view needs a template' )
            : $template;
    }
    /**
     * @param ModelsArgsInterface $args
     * @return int|void
     */
    public function render( ModelsArgsInterface $args )
    {
        if ( is_wp_error( $this->template ) )
            return print $this->template->get_error_message();

        /** @var $callback Closure */
        $callback = function( $template )
        {
            extract( get_object_vars( $this ) );
            require $template;
        };
        call_user_func(
            $callback->bindTo( $args ),
            $this->template
        );
    }
}

分離視圖和渲染

這意味着我們可以使用一個非常簡單的模板,如下所示

<!--suppress HtmlFormInputWithoutLabel -->
<p><?= $label ?></p>

呈現我們的內容。將這些片段放在一起,我們會得到以下幾點 (在我們的控制器,中介者等) 中:

namespace WeCodeMorePackage;

$view = new PackageView( plugin_dir_path( __FILE__ ).'tmpl/label.tmpl.php' );
$view->render( new ModelsArgs );

我們獲得了什麼?

這樣我們可以

  1. 輕鬆交換模板而不改變數據結構

  2. 有容易閲讀的文章

  3. 避免全球範圍

  4. 可以 Unit-Test

  5. 可以交換模型/數據,而不會損害其他組件

將 OOP PHP 與 WP API 相結合

當然這不是用 get_header()get_footer()等基本的主題功能呢,對吧?錯誤。只需要在任何你想要的模板或模板部分調用你的班級。渲染它,轉換數據,做任何你想要的。如果你真的很好,你甚至可以添加自己的一些自定義過濾器,並且有一些談判者來處理由哪個控制器渲染哪個路由/條件模板加載。

結論?

您可以在 WP 中使用如上所述的內容,而不會出現問題,並且仍然堅持使用基本的 API,並重新使用代碼和數據,而不會調用單個全局或者弄亂和污染全局名稱空間。

次佳解決方案

這是 @kaiser 答案的替代方法,我發現很好 (來自我的+1),但是需要額外的工作來與核心 WP 功能一起使用,並且它與模板層次結構集成在一起較低的 per-se 。

我想要分享的方法是基於單個類 (它是我正在開發的一個 stripped-down 版本),它負責處理模板的渲染數據。

它有一些 (IMO) 有趣的功能:

  • 模板是標準的 WordPress 模板文件 (single.php,page.php),它們有更多的功能

  • 現有的模板只是工作,所以您可以不用努力地集成現有主題的模板

  • @kaiser 方法不同,在模板中,您可以使用 $this 關鍵字訪問變量:這樣可以在未定義的變量的情況下避免生產中的通知

Engine

namespace GMTemplate;

class Engine
{
    private $data;
    private $template;
    private $debug = false;

  /**
   * Bootstrap rendering process. Should be called on 'template_redirect'.
   */
  public static function init()
  {
      add_filter('template_include', new static(), 99, 1);
  }

  /**
   * Constructor. Sets debug properties.
   */
  public function __construct()
  {
      $this->debug =
          (! defined('WP_DEBUG') || WP_DEBUG)
          && (! defined('WP_DEBUG_DISPLAY') || WP_DEBUG_DISPLAY);
  }

  /**
   * Render a template.
   * Data is set via filters (for main template) or passed to method for partials.
   * @param string $template template file path
   * @param array  $data     template data
   * @param bool   $partial  is the template a partial?
   * @return mixed|void
   */
  public function __invoke($template, array $data = array(), $partial = false)
  {
      if ($partial || $template) {
          $this->data = $partial
              ? $data
              : $this->provide(substr(basename($template), 0, -4));
          require $template;
          $partial or exit;
      }

      return $template;
  }

  /**
   * Render a partial.
   * Partial-specific data can be passed to method.
   * @param string $template template file path
   * @param array  $data     template data
   * @param bool   $isolated when true partial has no access on parent template context
   */
  public function partial($partial, array $data = array(), $isolated = false)
  {
      do_action("get_template_part_{$partial}", $partial, null);
      $file = locate_template("{$partial}.php");
      if ($file) {
          $class = __CLASS__;
          $template = new $class();
          $template_data =  $isolated ? $data : array_merge($this->data, $data);
          $template($file, $template_data, true);
      } elseif ($this->debug) {
          throw new RuntimeException("{$partial} is not a valid partial.");
      }
  }

  /**
   * Used in templates to access data.
   * @param string $name
   * @return string
   */
  public function __get($name)
  {
      if (array_key_exists($name, $this->data)) {
          return $this->data[$name];
      }
      if ($this->debug) {
          throw new RuntimeException("{$name} is undefined.");
      }

      return '';
  }

  /**
   * Provide data to templates using two filters hooks:
   * one generic and another query type specific.
   * @param string $type Template file name (without extension, e.g. "single")
   * @return array
   */
  private function provide($type)
  {
      $generic = apply_filters('gm_template_data', array(), $type);
      $specific = apply_filters("gm_template_data_{$type}", array());

      return array_merge(
        is_array($generic) ? $generic : array(),
        is_array($specific) ? $specific : array()
     );
  }
}

(在這裏提供 Gist)

如何使用

只需要調用 Engine::init()方法,大概在'template_redirect'鈎子上。這可以在主題 functions.php 或插件中完成。

require_once '/path/to/the/file/Engine.php';
add_action('template_redirect', array('GMTemplateEngine', 'init'), 99);

就這樣。

您現有的模板將以 expcted 的形式工作。但現在您可以訪問自定義模板數據。

自定義模板數據

要將自定義數據傳遞給模板,有兩個過濾器:

  • 'gm_template_data'

  • 'gm_template_data_{$type}'

第一個是針對所有模板觸發的,第二個是模板特定的,實際上,{$type}是文件擴展名的模板文件的基本名稱。

例如。過濾器'gm_template_data_single'可用於將數據傳遞給 single.php 模板。

附加到這些鈎子的回調必須返回一個數組,其中的鍵是變量名。

例如,您可以傳遞元數據作為模板數據,所以:

add_filter('gm_template_data', function($data) {
    if (is_singular()) {
        $id = get_queried_object_id();
        $data['extra_title'] = get_post_meta($id, "_theme_extra_title", true);
    }

    return $data;
};

然後,您可以在模板中使用:

<?= $this->extra_title ?>

調試模式

當常數 WP_DEBUGWP_DEBUG_DISPLAY 都為真時,類工作在調試模式。這意味着如果未定義變量,則會拋出異常。

當類不處於調試模式 (可能在生產中) 訪問未定義的變量將輸出一個空字符串。

數據模型

組織數據的一個好的和可維護的方式是使用模型類。

它們可以是非常簡單的類,它使用上述相同的過濾器返回數據。沒有特定的界面可以按照你的喜好進行組織。

在這下面,只是一個例子,但是你可以自己去做。

class SeoModel
{
  public function __invoke(array $data, $type = '')
  {
      switch ($type) {
          case 'front-page':
          case 'home':
            $data['seo_title'] = 'Welcome to my site';
            break;
          default:
            $data['seo_title'] = wp_title(' - ', false, 'right');
            break;
      }

      return $data;
  }
}

add_filter('gm_template_data', new SeoModel(), 10, 2);

__invoke()方法 (當類被用作回調時運行) 返回一個字符串,用於模板的<title> 標籤。

感謝'gm_template_data'傳遞的第二個參數是模板名稱,該方法返回主頁的自定義標題。

上面的代碼可以使用類似的東西

 <title><?= $this->seo_title ?></title>

在頁面的<head> 部分。

Partials

WordPress 具有 get_header()get_template_part()等功能,可用於將部分加載到主模板中。

使用 Engine 類時,這些函數就像所有其他 WordPress 函數一樣可以在模板中使用。

唯一的問題是,在使用核心 WordPress 功能加載的部分內部不可能使用使用 $this 獲取自定義模板數據的高級功能。

因此,Engine 類具有方法 partial(),允許加載部分 (以完全 child-theme 兼容的方式),並且仍然可以在部分自定義模板數據中使用。

用法非常簡單。

假設在主題 (或子主題) 文件夾中有一個名為 partials/content.php 的文件,它可以包括使用:

<?php $this->partial('partials/content') ?>

內部的這個部分將可能訪問所有父主題數據是一樣的。

與 WordPress 功能不同,Engine::partial()方法允許將特定數據傳遞給部分數據,只需將數據數組作為第二個參數傳遞。

<?php $this->partial('partials/content', array('greeting' => 'Welcome!')) ?>

默認情況下,partials 可以訪問父主題中可用的數據,並通過數據顯示。

如果顯式傳遞給 partial 的某個變量具有父主題變量的相同名稱,那麼明確傳遞的變量將獲勝。

然而,也可以包括部分隔離模式,即部分無法訪問父主題數據。要做到這一點,只需將 true 作為第三個參數傳遞給 partial()

<?php $this->partial('partials/content', array('greeting' => 'Welcome!'), true) ?>

Conclusion

即使相當簡單,Engine 類也相當完整,但肯定可以進一步改進。例如。沒有辦法檢查變量是否被定義。

由於其與 WordPress 功能和模板層次結構的 100%兼容性,您可以將其與現有和第三方代碼集成,沒有任何問題。

但是,請注意,僅部分測試,所以有可能有我還沒有發現的問題。

「我們獲得了什麼?在 @kaiser answer

  1. 輕鬆交換模板而不改變數據結構

  2. 有容易閲讀的文章

  3. 避免全球範圍

  4. 可以 Unit-Test

  5. 可以交換模型/數據,而不會損害其他組件

對我的班級也是有效的。

第三種解決方案

簡單的回答,不要在任何地方傳遞變量,因為它使用惡意的全局變量發臭。

從你的例子看,你似乎試圖做一個早期的優化,另一個邪惡的)

使用 wordpress API 獲取存儲在數據庫中的數據,並且不要嘗試超越並優化其使用,因為 API 只是檢索值並激活過濾器和操作。通過刪除 API 調用,您可以刪除其他開發人員在不修改代碼的情況下更改代碼行為的能力。

參考文獻

注:本文內容整合自 Google/Baidu/Bing 輔助翻譯的英文資料結果。如果您對結果不滿意,可以加入我們改善翻譯效果:薇曉朵技術論壇。