问题描述

我在 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 辅助翻译的英文资料结果。如果您对结果不满意,可以加入我们改善翻译效果:薇晓朵技术论坛。