/Hierarchy

No-dependencies package that embodies WordPress template hierarchy

Primary LanguagePHPMIT LicenseMIT

Hierarchy

PHP Quality Assurance codecov.io license release

Representation of the WordPress template hierarchy with PHP objects.


TOC


What / Why?

For every frontend request, WordPress runs a query, and then loads a template file depending on the query.

The map query => template follows rules defined in the template hierarchy.

However, given a query object, there's no way to programmatically:

  • know which template WordPress will load
  • know which templates WordPress will search
  • apply the same "query-to-template resolution" to a query

This library provides a way to do the 3 things listed above.

Template Hierarchy Representation

Given a query, this library provides a template hierarchy representation in form of a PHP array:

Example:

// we will show template hierarchy for the main query
global $wp_query; 

$hierarchy = new Brain\Hierarchy\Hierarchy();

var_export($hierarchy->hierarchy($wp_query));

Assuming we're visiting an URL like example.com/category/foo/page/2, and the category ID for the term "foo" is 123, the output of code above is:

array(
  'category' => array('category-foo', 'category-123', 'category'),
  'archive' => array('archive'),
  'paged' => array('paged'),
  'index' => array('index'),
);

And if you compare that array with the visual overview of template hierarchy you can verify what's above is an accurate representation of the template hierarchy for a category query.

Template Resolution

If the question you want to answer is:

Which templates WordPress will try to find for this query?

It can be simply answered using the Hierarchy::templates() method:

// we will target the main query
global $wp_query; 

$hierarchy = new Brain\Hierarchy\Hierarchy();

var_export($hierarchy->templates($wp_query));

Assuming the same query as above, the output will be:

array(
  'category-foo',
  'category-123',
  'category',
  'archive',
  'paged',
  'index',
);

That is the list of templates WordPress will search, in the same order that WordPress will use.

Template Resolution Example

For this example, I will assume that a theme has template files stored in /templates subfolder and using .phtml as file extension.

All the code necessary to load those templates according to template hierarchy is the following:

add_action('template_redirect', function() {

    $templates = (new Brain\Hierarchy\Hierarchy())->templates();
    
    foreach($templates as $template) {
      $path = get_theme_file_path("/templates/{$template}.phtml");
      if (file_exists($path)) {
         require $path;
         exit();
      }
    }
});

The example above works, and is just an example of what you can do with this library.

However, for the purpose to load templates, this library provides a specific class: QueryTemplate.

Introducing QueryTemplate

QueryTemplate class makes use of the Hierarchy class to get a list of templates to search, then it looks for those templates and loads the first found.

Example:

add_action('template_redirect', function(): void {

    global $wp_query;
    
    $queryTemplate = new \Brain\Hierarchy\QueryTemplate();
    echo $queryTemplate->loadTemplate($wp_query);
    
    exit();
    
});

The code above does exactly what WordPress does: the proper template is found searching in the theme folder and in parent theme folder (if current theme is a child theme) then the first template found is loaded and its content is printed to page.

Please note how template content is returned by QueryTemplate::loadTemplate(), so echo is necessary to actually display page content.

However, it is just the default behavior, and it can be customised.

Template Finders

By default, QueryTemplate class, searches for templates in theme (and parent theme, if any) folder, just like WordPress does.

However, it is possible to use a different "template finder" class to do something different.

All the template finder classes have to implement the Brain\Hierarchy\Finder\TemplateFinder interface.

The library comes with a few classes implementing that interface, and of course, it is possible to write a custom one.

Finder\ByFolders

The class Brain\Hierarchy\Finder\ByFolders can be used to search for templates in some arbitrary folders, instead of theme and parent theme folders.

Example:

add_action('template_redirect', function(): void {

    $finder = new \Brain\Hierarchy\Finder\ByFolders([
       __DIR__,
       get_stylesheet_directory(),
       get_template_directory(),
    ]);
    
    $queryTemplate = new \Brain\Hierarchy\QueryTemplate($finder);  
      
    echo $queryTemplate->loadTemplate();
    exit();
    
});

The snippet above will search for templates in the current folder and if templates are not found there, they are searched in theme and parent theme folders.

Custom file extensions

Finder\ByFolders class, by default, searches for files with .php extension, but it is possible to use different file extensions, by passing them as a second constructor argument (either a string or an array of strings):

// This will look for *.phtml files.
$phtml_finder = new \Brain\Hierarchy\Finder\ByFolders(
    [get_stylesheet_directory(), get_template_directory()],
    'phtml'
);

// This will look for Twig files first, and fall back to standard PHP files if
// no matching Twig file was found.
$twig_finder = new \Brain\Hierarchy\Finder\ByFolders(
    [get_stylesheet_directory(), get_template_directory()],
    'twig',
    'php'
);

Finder\BySubfolder

This template finder class is very similar to Brain\Hierarchy\Finder\ByFolders, however it looks for templates is a specific subfolder of theme (and parent theme) and use theme (and parent theme) folder as fallback:

add_action('template_redirect', function(): void {

    $finder = new \Brain\Hierarchy\Finder\BySubfolder('templates');

    $queryTemplate = new \Brain\Hierarchy\QueryTemplate($finder);

    echo $queryTemplate->loadTemplate();
    exit();
} );

Using code above the templates are searched, in order, in:

  • /path/to/wp-content/child-theme/templates/
  • /path/to/wp-content/parent-theme/templates/
  • /path/to/wp-content/child-theme/
  • /path/to/wp-content/parent-theme/

Finder\BySubfolder, just like Finder\ByFolders, accepts (a variadic number of) custom file extensions from the second constructor argument.

Finder\Localized

This finder class works in combination with another finder, and allows loading templates based on the current locale:

add_action('template_redirect', function(): void {

    $foldersFinder = new \Brain\Hierarchy\Finder\ByFolders();

    $finder = new \Brain\Hierarchy\Finder\Localized($foldersFinder);

    $queryTemplate = new \Brain\Hierarchy\QueryTemplate($finder);

    echo $queryTemplate->loadTemplate();
    exit();
} );

Assuming the current locale is it_IT, using code above, the templates are searched, in order, in:

  • /path/to/wp-content/child-theme/it_IT/
  • /path/to/wp-content/parent-theme/it_IT/
  • /path/to/wp-content/child-theme/it/
  • /path/to/wp-content/parent-theme/it/
  • /path/to/wp-content/child-theme/
  • /path/to/wp-content/parent-theme/

Finder\SymfonyFinderAdapter

This class allows to use the Symfony Finder Component to find templates:

add_action('template_redirect', function() {

    $symfonyFinder = new \Symfony\Component\Finder\Finder();
    $symfonyFinder = $symfonyFinder->files()->in(__DIR__);

    $finder = new \Brain\Hierarchy\Finder\SymfonyFinderAdapter($symfonyFinder);

    $queryTemplate = new \Brain\Hierarchy\QueryTemplate($finder);

    echo $queryTemplate->loadTemplate();
    exit();
} );

Finder\ByCallback

This class can be used to easily integrate 3rd party different loaders with QueryTemplate class.

In fact, you need to provide an arbitrary callback that will be called to find templates.

The callback will receive the template name without file extension, e.g. index and has to return the full path of the template if found, or an empty string if the template is not found.

Example:

add_action('template_redirect', function(): void {

    $callback = fn(string $tpl): string => realpath(__DIR__ . "{$template}.php") ?: '';

    $finder = new \Brain\Hierarchy\Finder\ByCallback($callback);

    $queryTemplate = new \Brain\Hierarchy\QueryTemplate($finder);

    echo $queryTemplate->loadTemplate();
    exit();
} );

Core Filters for Template Loading

When WordPress searches for a template in template-loader.php, it triggers different filters in the form of {$type}_template; examples are 'single_template'. 'page_template' and so on.

Moreover, the found template passes through the 'template_include' filter.

By default, QueryTemplate::loadTemplate() applies same filters, to maximize compatibility with core behavior.

That happens no matter the template finder is used.

However, by passing false as second argument to the method it will stop to apply those core filters.

Introducing Template Loaders

After a template is found with any of the finder classes, QueryTemplate has to "load" it.

By default, loading is just a require wrapped by ob_start() / ob_get_clean() so that the template content is returned as-is.

However, is it possible to process the template in some ways, for example, by using a template engine.

Custom template loaders have to implement Brain\Hierarchy\Loader\Loader interface, that has just one method: load(), that receives the full path of the template and have to return the template content.

Template loaders can be passed as second constructor argument to QueryTemplate.

Loader\FileRequire

This is the a loader class that ships with the library, and it provides the default behavior.

Aggregate Loaders

Aggregate loaders uses different "inner" loaders to load templates.

Aggregate loaders have to implement the interface Brain\Hierarchy\Loader\Aggregate that has two methods:

  • addLoader(Loader\TemplateLoader $loader, callable $predicate)
  • addLoaderFactory(callable $loaderFactory, callable $predicate)

The first is used to add a template loader instance. The second is used to add a factory that once called will return a template loader instance.

Both methods accept as second argument a "predicate": a callback that will receive the path of the template file to load, and will return a boolean.

When the predicate returns true, the related loader is used to load the template.

Loader\Cascade

Loader\Cascade is a simple implementation of an aggregate loader, where the predicates are evaluated in the same order they are added (FIFO).

Loader\ExtensionMap

Loader\ExtensionMap is another aggregate loader implementation shipped with Hierarchy.

It is used to load different loaders based on template file extension.

It requires an extensions-to-loaders "map" to be passed to constructor.

The map keys are the template file extensions, the values are the loader to be used.

Loaders can be passed as:

  • template loader instances
  • template loader fully qualified class names
  • factory callbacks that once called return template loader instances

The same loader can be used for multiple file extensions, using as map key a string composed by many file extensions separated by a pipe |.

Example:

$loader = new Loader\ExtensionMap([
    'php|phtml' => new Loader\FileRequire(),
    'mustache'  => fn() => new MyMustacheAdapter(new Mustache_Engine),
    'md' => MyMarkdownRenderer::class
]);

QueryTemplate Usage Example: Loading and Rendering Mustache Templates

The following will present all the code necessary to find and render mustache templates according to WordPress template hierarchy.

namespace My\Theme;

use Brain\Hierarchy\{Finder, Loader, QueryTemplate};

class MustacheTemplateLoader implements Loader\Loader
{
   private $engine;

   public function __construct(\Mustache_Engine $engine)
   {
      $this->engine = $engine;
   }

   public function load(string $templatePath): string
   {
        // let's use a filter to build some context for the template
        $data = apply_filters('my_theme_data', ['query' => $GLOBALS['wp_query'], $templatePath);

        return $this->engine->render(file_get_contents($templatePath), $data);
   }
}

add_action('template_redirect', function() {
    if (!QueryTemplate::mainQueryTemplateAllowed()) {
        return;
    }

    $queryTemplate = new QueryTemplate(
        // will look for "*.mustache" templates in theme's "/templates" subfolder
        new Finder\BySubfolder('templates', 'mustache'),
        // the loader class defined above
        new MustacheTemplateLoader(new \Mustache_Engine())
    );

    // 3rd argument of loadTemplate() is passed by reference, and set to true if template is found
    $content = $queryTemplate->loadTemplate(null, true, $found);
    // if template was found, let's output it and exit, otherwise WordPress will continue its work
    $found and die($content);
});

Requirements

Hierarchy requires PHP 7.1.3+ and Composer to be installed.

Installation

Best served by Composer, available on Packagist with name brain/hierarchy.

Migration from version 2.*

The library logic in version 3 is not changed, but now all classes use type declaration, and some of them has been renamed.

Libraries based on Hierarchy which are implementing its loader/finder interfaces will have some work to do to rename classes and add type declaration.

The class FileExtensionPredicate has a slightly changed signature.

Libraries which are only using Hierarchy class, should work without any change, even if the methods getHierarchy() and getTemplates() are now deprecated in favor of, respectively, hierarchy() and templates(), but old method will not removed in any version 3.* release.

License

Hierarchy is released under MIT.