The New Myth family: SprintPHP Bonfire Practical CodeIgniter 3

New Myth Media Blog

Serving the New Myth Media Family.

Practical CodeIgniter 3 Released

My new book about making the most of CodeIgniter 3 is out!

One of the often-requested features for CodeIgniter is a built-in template engine. For reasons I don't need to get into here, that's not something that will happen. All is not lost, though! The Parser in CI4 has been vastly upgraded since the previous version, and allows near infinite expandability to its feature set through filters and plugins. This post will take advantage of the Plugins feature to show how to implement two of the most important parts of a template engine: extending templates and content blocks within those templates.

NOTE: The example code is not feature complete, doesn’t do any security checks, and might be a bit hacky in places. It’s meant as a proof-of-concept and jumping-off block for you to understand working with plugins.

Initial Setup

Even though this is an example, I want to treat it as much like a real project as possible, so first let’s create a new namespace for the template engine and get our folder structures setup so that we’re ready to go.

I would keep this in its own module that’s easily reusable. We’ll keep things simple and go with Myth\Template as the main namespace, since I use Myth for much of my open-source stuff nowadays. We will keep this in the main directory, right alongside the application and system folders:

/application
/system
/Myth
    /Template

Only thing left to do is to let CodeIgniter know where it’s found, so open application/Config/Autoload.php and the new namespace to the $psr4 array in the class constructor:

`$psr4 = [
    'Config' => APPPATH.'Config',
    'App'  => APPPATH,
    'Myth\Template' => ROOTPATH.'Myth/Template'
];

Registering the Plugins

Every Parser plugin must be registered within Config\View.php in order for the Parser to find the plugins. When writing a larger add-on like this that would eventually combine a number of different plugins, it becomes quite cumbersome to have to enter every plugin for this manually. For example, every feature within the template system, like extend, block, and insert commands, need to be implemented as their own plugin. There is an easier way, though.

A recently added feature allows any config class (that extends from BaseConfig) to support Registrar classes. These classes simply add any number of entries to the config file. A single Registrar class can be used for any number of configuration files, making them ideal for shipping with bundles/modules/packages.

Before we go into the details on how they work, we need to ensure the View config file knows about it. Add the following to application/Config/View.php:

protected $registrars = [
    MythTemplateRegistrar::class
];

All classes listed here will be looped over, calling a static method within it that matches the name of the Config file, without the namespace and extension. In this case, our config file is View.php, so there should be a static method called View(). Create the class now at Myth/Template/Registrar.php and add the following code:

<?php namespace MythTemplate;

class Registrar
{
    public static function View()
    {
        return [
            'plugins' => [
                'extend' => [ function($str, array $params=[]) { return Engine::instance()->extend($str, $params); } ],
                'block'  => [ function($str, array $params=[]) { return Engine::instance()->block($str, $params); } ],
            ]
        ];
    }
}

This registers two new plugins, extend and block. Both of these plugins expect to work with the content between opening and closing tags, so the functions themselves are wrapped within an array. The values of each of these can be any type of valid callable. In this case, we’re using closures that grab an instance of the main template engine class, and run the appropriate method. Nothing too complex here.

The Engine

The template engine itself is a single class file, called Engine. Here’s the skeleton class:

<?php namespace MythTemplate;

use Config\Services;

class Engine
{
    protected static $instance;
    protected $parser;
    protected $blocks = [];

    public function __construct()
    {
        $this->parser = Services::parser();
    }

    public static function instance()
    {
        if (is_null(self::$instance))
        {
            self::$instance = new Engine();
        }

        return self::$instance;
    }

    public function extend(string $body, array $params = []) { }

    public function block(string $body, array $params=[]) { }

}

Before diving into the details of the methods themselves, let’s look at the parts we are working with.

First up is the static $instance class variable. This simply maintains a singleton of the class. Since the blocks and template are both rendered at different times, we need a single place to store the bits and pieces of the final page until we are ready to put it all together. Using a singleton gets us what we need. The instance is retrieved through the instance() method, as seen in the plugin definition.

In the constructor we grab a copy of CodeIgniter’s Parser and store it so we don’t have to keep instantiating it. While it’s done explicitly here, a better solution would be to pass it in through the constructor but that is left up to you to implement.

We have a $blocks array that stores all of the rendered blocks that need to be inserted into the layout file.

And, finally, we have the two methods that we call from the plugin definitions. We’ll look at those now.

{+ extend +}

The purpose of extend is to define which main layout/template the content of this view should be inserted into. So, it should accept a filename as it’s only parameter that we’ll call tpl. Due to the way plugins work if you want to use the content, we must wrap the content in opening and closing tag pairs, so we would end up doing something like this in our views:

{+ extend tpl=master +}
    ... custom content here ...
{+ /extend +}

NOTE: While not implemented just yet, the Parser class will be updated in the future to remove the need to name the parameters, but we’ll show what works currently here.

public function extend(string $body, array $params = [])
{
    if (! array_key_exists('tpl', $params))
    {
        throw new BadMethodCallException('Must provide tpl value to extend a template.');
    }

    // Parse the contents since it will likely be
    // filling blocks inside the template itself.
    $this->parser->renderString($body, null, true);

    // Parse our template last, so that all child
    // blocks will have had time to populate this->blocks.
    $out = $this->parser->render($params['tpl'], null, true);

    return $out;
}

When this plugin is called, $body holds all of the HTML between the opening and closing tags. We first verify that the tpl param exists or throw an exception. Next, we run the body through the parser itself. We have to do this so that any plugins will be ran on that page, the most important for us right now are any calls to {+ block +} which will store the block data locally in the Engine class.

Finally, we render the template itself, which will output the contents of any blocks that have been overridden in our views, merging everything together. The result of that merging is returned, where it gets displayed in the browser.

{+ block +}

The block plugin must operate in two separate ways. First, it must work within the individual views to allow the default contents of the block in the template to be overridden on a per-view basis. Second, it must work within the template files to spit any overridden content out. If no overridden content exists, it should show the default data.

A simple template file might be:

<html>
    <body>
        {+ block name=content +}
            <h1>Default Content\</h1>
        {+ /block +}
    </body>
</html>

And an individual view file might look like:

{+ extend tpl=master +}

    {+ block name=content +}
        <h1>New Content\</h1>
    (+ /block +}

(+ /extend +}

So, let’s see how that could be implemented:

public function block(string $body, array $params=[])
{
    if (! array_key_exists('name', $params))
    {
        throw new BadMethodCallException('Template blocks must have a name');
    }

    $name = $params['name'];

    // If we already have content for that (from a child view)
    // then simply return it.
    if (array_key_exists($name, $this->blocks)) return $this->blocks[$name];

    // Otherwise, save the content locally since we're inserting that
    // into the parent template somewhere.
    $this->blocks[$name] = $body;

    return $body;
}

First, we ensure the name param exists. Now, if we already have the content (which means we’re in a template file) return the existing content. Otherwise, store the body we were given (the part between the opening and closing block tags), and return it. That way, whether we’re in the view file or the template, we will show the right content: either the overridden content or the default.

By no means is this a perfect solution, and it’s only the start of what a full-featured template engine would need, but I hope it helps you understand Parser plugins a bit more, and maybe gets you inspired to band together to create a new template engine that might be adopted by the CI team. :)