16

I have a variable in header.php, such as:

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

Once I do:

var_dump($page_extra_title);

I always get NULL outside of header.php (var_dump works properly in header.php only). I've been pasting the same variable everywhere I need it (page.php, post.php, footer.php etc.), but it's madness and makes everything almost impossible to maintain.

I'm wondering what's the best way of passing a variable through all the files in my theme? I guess using functions.php along with "get_post_meta" might not be the best idea? :)

  • 1
    PHP Manual: Variable Scope – Nicolai Apr 14 '15 at 22:03
  • I do think the variable is in the same scope, also I want to avoid using GLOBAL for obvious reasons. – Wordpressor Apr 14 '15 at 22:04
  • I believe ialocin's comment is spot on. One PHP script doesn't know the other exists and cannot access it's local variables or their values. – jdm2112 Apr 14 '15 at 22:05
  • 1
    header and footer are included via a function, so the scope of everything in those files is those function's scopes. – Milo Apr 14 '15 at 22:06
  • 4
    Don't shoot the messenger :) The only thing I said is, it is indeed a scope problem. There is a way, it is global, right? But it is out of the question for good reasons. Besides you kind of have to "call" global-variables too, by using the keyword to make them available. Depending on the use case sessions might be a solution. Otherwise - as mentioned - I think a function or a class to do the job for you is the way to go. – Nicolai Apr 14 '15 at 22:18
10

Basic separated data structures

To pass around data, you normally utilize a Model (that's the "M" in "MVC"). Let's look at a very simple interface for data. Interfaces are just used as "Recipes" for our building blocks:

namespace WeCodeMore\Package\Models;
interface ArgsInterface
{
    public function getID();
    public function getLabel();
}

Above is what we pass around: A common ID and a "Label".

Displaying data by combining atomic pieces

Next we need some View that negotiates between our Model and ... our template.

namespace WeCodeMore\Package;
interface PackageViewInterface
{
    /**
     * @param Models\ArgsInterface $args
     * @return int|void
     */
    public function render( Models\ArgsInterface $args );
}

Basically that Interface says

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

Finally we need to implement above and build the actual View. As you can see, the constructor tells that the mandatory thing for our view is a Template and that we can render it. For the sake of easy development we even check if the template file actually is present so we can make other developers lives (and ours as well) much easier and note that.

In a second step in the render function we use a Closure to build the actual template wrapper and bindTo() the Model to the template.

namespace WeCodeMore\Package;

use WeCodeMore\Package\Models\ArgsInterface;

/** @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 Models\ArgsInterface $args
     * @return int|void
     */
    public function render( Models\ArgsInterface $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
        );
    }
}

Separating the View and Rendering

This means that we can use a very simple template like the following

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

to render our content. Putting the pieces together we would get something around the following lines (in our Controller, Mediator, etc.):

namespace WeCodeMore\Package;

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

What did we gain?

This way we can

  1. Easily exchange templates without changing the data structure
  2. Have easy to read tempaltes
  3. Avoid global scope
  4. Can Unit-Test
  5. Can exchange the Model/the data without harming other components

Combining OOP PHP with the WP API

Of course this is hardly possible by using basic theming functionality like get_header(), get_footer(), etc., right? Wrong. Just call your classes in whatever template or template part you would like. Render it, transform the data, do whatever you want. If you are really nice you even just add your own bunch of custom filters and have some negotiator to take care of what gets rendered by which controller on which route/conditional template load.

Conclusion?

You can work with stuff like above in WP without a problem and still stick to the basic API and reuse code and data without calling a single global or messing up and polluting the global name space.

  • 3
    Looks great! I'll look more into this, nice answer! – marko Apr 15 '15 at 7:11
  • @kaiser almost 3 years later, are there any updates to your thinking above? WP core templating hasn't really progressed in any more advanced direction, so 3rd-party solutions are still a thing. – lkraav Dec 21 '17 at 7:20
  • 1
    @Ikraav I would probably not write it like this nowadays, but I am still certain that not using a separate syntax to output the contents of variables inside HTML tags is the way to go (and avoids an unnecessary overhead). On the other hand, I am rarely writing frontend stuff in PHP these days, but in JavaScript. And I really like what VueJS and friends are bringing to the table. – kaiser Dec 23 '17 at 16:33
11

This is an alternative approach to @kaiser answer, that I found pretty fine (+1 from me) but requires additional work to be used with core WP functions and it's per-se low integrated with template hierarchy.

The approach I want to share is based on a single class (it's a stripped-down version from something I'm working on) that takes care of render data for templates.

It has some (IMO) interesting features:

  • templates are standard WordPress template files (single.php, page.php) they get a bit of more power
  • existing templates just work, so you can integrate template from existent themes with no effort
  • unlike @kaiser approach, in templates you access variables using $this keyword: this gives you the possibility to avoid notices in production in case of undefined variables

The Engine Class

namespace GM\Template;

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()
     );
  }
}

(Available as Gist here.)

How to use

Only thing needed is to call the Engine::init() method, probably on 'template_redirect' hook. That can be done in theme functions.php or from a plugin.

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

That's all.

Your existing templates will work as expcted. But now you have the possibility to access custom template data.

Custom Template Data

To pass custom data to templates there are two filters:

  • 'gm_template_data'
  • 'gm_template_data_{$type}'

The first one is fired for all templates, the second is template specific, in fact, the dymamic part {$type} is the basename of the template file without file extension.

E.g. the filter 'gm_template_data_single' can be used to pass data to the single.php template.

The callbacks attached to these hooks have to return an array, where the keys are the variable names.

For example, you can pass meta data as template data likes so:

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;
};

And then, inside the template you can just use:

<?= $this->extra_title ?>

Debug Mode

When both the constants WP_DEBUG and WP_DEBUG_DISPLAY are true, the class worksin debug mode. It means that if a variable is not defined an exception is thrown.

When the class is not in debug mode (probably in production) accessing an undefined variable will output an empty string.

Data Models

A nice and maintenable way to organize your data is to use model classes.

They can be very simple classes, that return data using same filters described above. There is no particular interface to follow, they can be organized accordi to your preference.

Belowe, there is just an example, but you are free to do in your own way.

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);

The __invoke() method (that runs when a class is used like a callback) returns a string to be used for the <title> tag of the template.

Thanks to the fact that the second argument passed by 'gm_template_data' is the template name, the method returns a custom title for home page.

Having the code above, is then possible to use something like

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

in the <head> section of the page.

Partials

WordPress has functions like get_header() or get_template_part() that can be used to load partials into main template.

These functions, just like all the other WordPress functions, can be used in templates when using the Engine class.

The only problem is that inside the partials loaded using the core WordPress functions is not possible to use the advanced feature of getting custom template data using $this.

For this reason, the Engine class has a method partial() that allows to load a partial (in a fully child-theme compatible way) and still be able to use in partials the custom template data.

The usage is pretty simple.

Assuming there is a file named partials/content.php inside theme (or child theme) folder, it can be included using:

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

Inside that partial will be possible to access all parent theme data is the same way.

Unlike WordPress functions, Engine::partial() method allows to pass specific data to partials, simply passing an array of data as second argument.

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

By default, partials have access to data available in parent theme and to data explicilty passed .

If some variable explicitly passed to partial has the same name of a parent theme variable, then the variable explicitly passed wins.

However, is also possible to include a partial in isolated mode, i.e. the partial has no access to parent theme data. To do that, just pass true as third argument to partial():

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

Conclusion

Even if pretty simple, Engine class is pretty complete, but surely can be further improved. E.g. there is no way to check if a variable is defined or not.

Thanks to its 100% compatibility with WordPress features and template hierarchy you can integrate it with existing and third party code with no issue.

However, note that is only partially tested, so is possible there are issues I have not discovered yet.

The five points under "What did we gain?" in @kaiser answer:

  1. Easily exchange templates without changing the data structure
  2. Have easy to read tempaltes
  3. Avoid global scope
  4. Can Unit-Test
  5. Can exchange the Model/the data without harming other components

are all valid for my class as well.

  • 1
    Hehe. Well done, mate :) +1 – kaiser Apr 17 '15 at 8:32
  • @gmazzap almost 3 years later, are there any updates to your thinking above? WP core templating hasn't really progressed in any more advanced direction, so 3rd-party solutions are still a thing. – lkraav Dec 21 '17 at 7:21
  • 1
    I don't do many themes work these days. Lately my way to go was combining github.com/Brain-WP/Context + github.com/Brain-WP/Hierarchy to build data and pass to templates. For the template engine itself, I used different approaches, Foil (of course), Mustache, but also Twig (only when I had control on the whole webiste to avoid dependency hell) @lkraav – gmazzap Dec 21 '17 at 16:16
5

Simple answer, don't pass variables anywhere as it stinks of using global variables which is evil.

From your example it seems like you are trying to do an early optimization, yet another evil ;)

Use the wordpress API to get data which is stored in the DB and don't try to outsmart and optimize its usage as the API do more then just retrieving values and it activates filters and actions. By removing the API call you remove the ability of other developers to change the behavior of your code without modifying it.

2

Although kaiser's answer is technically right, I doubt it's the best answer for you.

If you're creating your own theme, then I think it is indeed the best way to set up some sort of framework using classes (and maybe namespaces and interfaces too, although that might be a little too much for a WP theme).

On the other hand, if you are just extending / adjusting an existing theme and only need to pass one or a few variables, I think you should stick with global. Because header.php is included within a function, the variables you declare in that file are usable in that file only. With global you make them accessible in the whole WP project:

In header.php:

global $page_extra_title;

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

In single.php (for example):

global $page_extra_title;

var_dump( $page_extra_title );
  • 3
    I don't want to be rude or anything, but it is really bad practice diving into the global scope. You should avoid adding to the global scope completely. You have to be really careful of naming conventions here and you need to make sure that your variable name is going to be unique in such a way that no one else can reproduce such a name. @kaiser approach might seem overboard for you, but it is by far the best and safest. I cannot tell you how to tackle this, but I really do advice you to stay out of the global scope :-) – Pieter Goosen Apr 15 '15 at 6:54
  • 3
    Of course you need to be careful that you do not overwrite other variables. You can tackle that by using an unique prefix or an array with your custom variables, $wp_theme_vars_page_extra_title or $wp_theme_vars['page_extra_title'] for example. It was just an explanation why global would work here. OP asked a way of passing a variable through all the files, using global is a way to do that. – redelschaap Apr 15 '15 at 7:30
  • 2
    No, globals is not a way to do that. There are far better ways to achieve the same without using globals. As I said before, and as @kaiser stated in his answer, avoid the global scope and stay out of it. Just as an example, take this very easy alternative, wrap your code in a function and call the function where needed. This way, you don't need to set or use a global. – Pieter Goosen Apr 15 '15 at 7:40
  • 3
    Yes it is. It might be not the best way, but it is definitely a way. – redelschaap Apr 15 '15 at 8:38
  • 2
    but it is really bad practice diving into the global scope I wish somebody told that to WP core developers. I really don't understand the point of using namespaces, data abstraction, design patterns, unit testing, and other programming best practices/techniques in code written for Wordpress when Wordpress core is littered with bad coding practices like glabal variables (e.g., the widgets code). – Ejaz Aug 14 '15 at 11:24
1

An easy solution is to write a function to get the extra title. I use a static variable to keep the database calls to one only. Put this in your functions.php.

function get_extra_title($post_id) {
    static $title = null;
    if ($title === null) {
        $title = get_post_meta($post_id, "_theme_extra_title", true)
    }
    return $title;
}

Outside header.php, call the function to get the value:

var_dump(get_extra_title($post->ID));

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.