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:
- Easily exchange templates without changing the data structure
- Have easy to read tempaltes
- Avoid global scope
- Can Unit-Test
- Can exchange the Model/the data without harming other components
are all valid for my class as well.
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.