Best way of passing PHP variable between partials?

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? 🙂

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

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 WeCodeMorePackageModels;
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 WeCodeMorePackage;
interface PackageViewInterface
{
    /**
     * @param ModelsArgsInterface $args
     * @return int|void
     */
    public function render( ModelsArgsInterface $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 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
        );
    }
}

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

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

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.

Method 2

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

(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('GMTemplateEngine', '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.

Method 3

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.

Method 4

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

Method 5

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


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x