In the 7th post of this series we are going to talk about a very important component for modern PHP frameworks. This component is usually hidden under a few abstractions to make the life of framework users easier, we are talking about the Templating component.

Templating engines allow to create representations of data

The Templating component

The Templating component provides an engine-agnostic infrastructure to load template files and render them using the input variables. Although the component provides methods for creating template systems, we are going to focus on how to integrate third-party template engines like Twig and Smarty, and how to use the right engine based on the templates file extension.

Templates using PHP

Using PHP to generate templates has been common practice in the PHP community and although this approach has a few advantages, such as not having to learn another language and that templates can be executed without having to compile them, there are also some drawbacks. Some of the drawbacks are:

  • PHP templates are usually more verbose, especially when dealing with output escaping:
    <?php echo htmlspecialchars($var, ENT_QUOTES, 'UTF-8') ?>
    
  • Missing features such as template inheritance, layouts, filters, and extensions.
  • Difficult for web designers who don’t know PHP.

The component provides a default engine to use PHP for templates improving some of these limitations and providing ways to escape output easily, include subtemplates, and common helpers.

Let’s create the simplest template possible, one that says hello using the variable $name, which is passed by the controller:

Hello <?php echo $name ?>! From a humble PHP template.

Then, to render the template:

<?php

use Symfony\Component\Templating\PhpEngine;
use Symfony\Component\Templating\TemplateNameParser;
use Symfony\Component\Templating\Loader\FilesystemLoader;

include_once __DIR__. '/vendor/autoload.php';

$loader = new FilesystemLoader(__DIR__.'/views/%name%');
$templating = new PhpEngine(new TemplateNameParser(), $loader);

echo $templating->render('hello.php', array('name' => 'Raul'));

It outputs:

Hello Raul! From a humble PHP template.

Here’s an overview of what happened under the hood. First, we create the PhpEngine instance with the required parameters, a couple of objects implementing the TemplateNameParserInterface and LoaderInterface interfaces. Don’t be afraid by all this code, it’s much simpler than it seems.

$loader is an instance of FilesystemLoader, it defines where the templates are – in our example, templates are in the view directory. The TemplateNameParser object converts a string containing the template filename to a TemplateReference object.

Finally, we execute the render method, which renders the template ‘views/hello.php’ with the ‘name’ variable set to ‘Raul’.

This complex setup is the price we pay when working with Symfony components, but as we will see in the next section, this design makes adding new templating engines really easy.

Adding other template engines

When adding new template engines, we create bridges that connect the engine itself with the component. This is done implementing the three methods defined in the EngineInterface interface:

  • render($name, array $parameters = array()): It’s the method in charge of actually rendering the template with the name $name passing the array $parameters as variables.
  • exists($name): Checks if the template with the name $name exists.
  • supports($name): Returns true if the engine bridge is able to process the template with the name $name. Usually, this is done checking the file extension. When there are several engine bridges this method is called to determine which one to use.

So, if we want to add support for Twig templates we just have to implement these methods.

<?php

namespace RaulFraile\Templating;

use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Templating\TemplateReferenceInterface;
use Symfony\Component\Templating\TemplateNameParserInterface;

use Twig_Loader_Filesystem;
use Twig_Environment;

class TwigEngine implements EngineInterface
{

    protected $environment;
    protected $parser;

    /**
     * Constructor.
     *
     * @param \Twig_Environment           $environment A \Twig_Environment instance
     * @param TemplateNameParserInterface $parser      A TemplateNameParserInterface instance
     * @param string                      $dir         Templates directory
     */
    public function __construct(\Twig_Environment $environment, TemplateNameParserInterface $parser, $dir)
    {
        $loader = new Twig_Loader_Filesystem($dir);

        $this->environment = $environment;
        $this->environment->setLoader($loader);

        $this->parser = $parser;
    }

    /**
     * Renders a template.
     *
     * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance
     * @param array $parameters An array of parameters to pass to the template
     *
     * @return string The evaluated template as a string
     *
     * @throws \RuntimeException if the template cannot be rendered
     *
     * @api
     */
    public function render($name, array $parameters = array())
    {
        return $this->environment->loadTemplate((string) $name)->render($parameters);
    }

    /**
     * Returns true if the template exists.
     *
     * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance
     *
     * @return Boolean true if the template exists, false otherwise
     *
     * @throws \RuntimeException if the engine cannot handle the template name
     *
     * @api
     */
    public function exists($name)
    {
        $loader = $this->environment->getLoader();

        return $loader->exists($name);
    }

    /**
     * Returns true if this class is able to render the given template.
     *
     * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance
     *
     * @return Boolean true if this class supports the given template, false otherwise
     *
     * @api
     */
    public function supports($name)
    {
        if ($name instanceof \Twig_Template) {
            return true;
        }

        $template = $this->parser->parse($name);

        return 'twig' === $template->get('engine');
    }

}

To be able to use dependency injection later, the newly created TwigEngine needs an instance of the Twig_Environment and an object that implements TemplateNameParserInterface, which is used to get the engine name – based on the extension. It also accepts a directory where the templates can be found.

The same could be applied to any engine we want to support. For example, a simple engine bridge for Smarty templates would look like this:

<?php

namespace RaulFraile\Templating;

use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Templating\TemplateReferenceInterface;
use Symfony\Component\Templating\TemplateNameParserInterface;
use Smarty;

class SmartyEngine implements EngineInterface
{

    protected $smarty;
    protected $parser;

    /**
     * Constructor.
     *
     * @param Smarty                      $environment A Smarty instance
     * @param TemplateNameParserInterface $parser      A TemplateNameParserInterface instance
     * @param string                      $dir         Templates directory
     */
    public function __construct(Smarty $smarty, TemplateNameParserInterface $parser, $dir)
    {
        $this->smarty = $smarty;
        $this->smarty->setTemplateDir($dir);

        $this->parser = $parser;
    }

    /**
     * Renders a template.
     *
     * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance
     * @param array $parameters An array of parameters to pass to the template
     *
     * @return string The evaluated template as a string
     *
     * @throws \RuntimeException if the template cannot be rendered
     *
     * @api
     */
    public function render($name, array $parameters = array())
    {
        $this->smarty->assign($parameters);

        return $this->smarty->fetch($name);
    }

    /**
     * Returns true if the template exists.
     *
     * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance
     *
     * @return Boolean true if the template exists, false otherwise
     *
     * @throws \RuntimeException if the engine cannot handle the template name
     *
     * @api
     */
    public function exists($name)
    {
        return $this->smarty->templateExists($name);
    }

    /**
     * Returns true if this class is able to render the given template.
     *
     * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance
     *
     * @return Boolean true if this class supports the given template, false otherwise
     *
     * @api
     */
    public function supports($name)
    {
        $template = $this->parser->parse($name);

        return 'tpl' === $template->get('engine');
    }

}

To use them in a controller:

<?php

use Symfony\Component\Templating\TemplateNameParser;
use RaulFraile\Templating\TwigEngine;
use RaulFraile\Templating\SmartyEngine;

include_once __DIR__. '/vendor/autoload.php';

$templatesDir = __DIR__.'/views/';
$templateNameParser = new TemplateNameParser();

$templating = new TwigEngine(new Twig_Environment(), $templateNameParser, $templatesDir),
// or $templating = new SmartyEngine(new \Smarty(), $templateNameParser, $templatesDir);

echo $templating->render('hello.php', array('name' => 'Raul'));
// or echo $templating->render('hello.tpl', array('name' => 'Raul'));

Choosing the right engine

We have a few engine bridges, but how to choose the right one? Frameworks usually allow us to use the template engine of our choice. For example, in Symfony2 full-stack framework, we can use both Twig and PHP templates simply by changing the filename:

$this->render('AcmeBundle:Hello:index.html.twig', array('name' => $name));

// or

$this->render('AcmeBundle:Hello:index.html.php', array('name' => $name));

Symfony knows which engine to use based on the extension. The component provides a way to follow this approach using the DelegatingEngine class and the supports() method of the engine bridges that we just implemented.

<?php

use Symfony\Component\Templating\PhpEngine;
use Symfony\Component\Templating\TemplateNameParser;
use Symfony\Component\Templating\Loader\FilesystemLoader;
use Symfony\Component\Templating\DelegatingEngine;

use RaulFraile\Templating\TwigEngine;
use RaulFraile\Templating\SmartyEngine;

include_once __DIR__. '/vendor/autoload.php';

$templatesDir = __DIR__.'/views/';
$templateNameParser = new TemplateNameParser();

$engines = new DelegatingEngine(array(
    new PhpEngine($templateNameParser, new FilesystemLoader($templatesDir . '%name%')),
    new TwigEngine(new Twig_Environment(), $templateNameParser, $templatesDir),
    new SmartyEngine(new \Smarty(), $templateNameParser, $templatesDir)
));

echo $engines->render('hello.php', array('name' => 'Raul')) . PHP_EOL;

The render() method of DelegatingEngine will iterate over all the defined engine bridges and call to their supports() method. As soon as it finds an engine capable of handling that template, executes the render() method of that engine.

An alternative for PHP templates: Plates

If you prefer to use native PHP templates but miss some features of compiled ones such as template inheritance, layouts or plugins/extensions, it is worth taking a look at Plates, which is a PHP templating system inspired by Twig.

If we want to provide Plates as the default engine for PHP templates, all we have to do is create a new engine that implements the EngineInterface interface and accepts ‘*.php’ templates.

<?php

namespace RaulFraile\Templating;

use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Templating\TemplateReferenceInterface;
use Symfony\Component\Templating\TemplateNameParserInterface;
use League\Plates;

class PlatesEngine implements EngineInterface
{

    protected $engine;
    protected $parser;

    public function __construct(Plates\Engine $engine, TemplateNameParserInterface $parser, $dir)
    {
        $this->engine = $engine;
        $this->engine->setDirectory($dir);
        $this->engine->setFileExtension(null);

        $this->parser = $parser;
    }

    public function render($name, array $parameters = array())
    {
        $template = new Plates\Template($this->engine);
        $template->data($parameters);

        return $template->render($name);
    }

    public function exists($name)
    {
        return $this->engine->pathExists($name);
    }

    public function supports($name)
    {
        $template = $this->parser->parse($name);

        return 'php' === $template->get('engine');
    }
}

Calling the setFileExtension() method with null is necessary in our example as we are already using the extension to choose the template engine, so we want Plates to use hello.php instead of hello.php.php.

Then, we just replace the PhpEngine object by the PlatesEngine one.

<?php

use Symfony\Component\Templating\TemplateNameParser;
use Symfony\Component\Templating\DelegatingEngine;

use RaulFraile\Templating\TwigEngine;
use RaulFraile\Templating\SmartyEngine;
use RaulFraile\Templating\PlatesEngine;

use League\Plates;

include_once __DIR__. '/vendor/autoload.php';

$templatesDir = __DIR__.'/views/';
$templateNameParser = new TemplateNameParser();

$engines = new DelegatingEngine(array(
    new PlatesEngine(new Plates\Engine(), $templateNameParser, $templatesDir),
    new TwigEngine(new Twig_Environment(), $templateNameParser, $templatesDir),
    new SmartyEngine(new \Smarty(), $templateNameParser, $templatesDir)
));

echo $engines->render('hello.php', array('name' => 'Raul')) . PHP_EOL;

Even though the template is the same, we can now use all the features of Plates.

Who’s using it?

Conclusion

The Templating component comes very handy when we need to provide different template engines, especially in frameworks and projects that can be extended somehow with templates, such as static blog generators or plugin systems.

More info

Photo: Matt Cornock