Обзор компонентов Symfony2 : Templating

Компонент Шаблонизатор (Templating)

Как правило, для отрисовки самих шаблонов с использованием переменных, этот компонент использует какой-либо движок шаблонизатора. Хотя сам компонент позволяет создавать собственные шаблонизаторы, мы с вами узнаем, как правильно интегрировать сторонний движок, например Twig или Smarty. А также настроим загрузку правильного шаблонизатора в зависимости от расширения файла шаблона.

Шаблонизатор с использованием PHP

Использование PHP в шаблонах — это очень распространенная практика. И хотя такой способ имеет свои преимущества, например, не надо изучать новый язык, не надо компилировать файл шаблона, но у него есть и несколько недостатков. К ним относятся:

  • Обычно шаблоны на PHP довольно сложны в использовании, особенно когда приходится фильтровать вывод <?php echo htmlspecialchars($var, ENT_QUOTES, 'UTF-8') ?>
  • Отсутствие наследования шаблонов, базовых шаблонов, фильтров и расширений
  • Довольно сложны в использовании для веб дизайнеров

Компонент Templating имеет в своем составе движок на PHP, который устраняет некоторые из перечисленных недостатков, например, он упрощает фильтрацию вывода, допускает наследование шаблонов и имеет встроенные вспомогательные функции.

Давайте создадим самый простой шаблон, который выводит переменную переданную в контроллер.

Hello <?php echo $name ?>! From a humble PHP 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'));

и вот что мы получим на выходе:

Hello Raul! From a humble PHP template.

Давайте разберемся в том, что здесь происходит. Для начала мы создали экземпляр PhpEngine передав все необходимые переменные (объекты реализующие интерфейсы TemplateNameParserInterface и LoaderInterface). Хотя этот код и выглядит устрашающе, ничего сложного в нем нет.

$loader — объект класса FilesystemLoader определяет, где находятся файлы шаблонов — в нашем примере это каталог views. Объект TemplateNameParser конвертирует строку с адресом шаблона в объект TemplateReference.

И, наконец, выполняем метод render() и отрисовываем шаблон views/hello.php с переменной nameимеющая значение Raul.

Такая сложная настройка — это цена, которую приходится платить при работе с Symfony2, но как мы увидим далее, отлично спроектированная структура позволяет нам с легкостью добавлять новые шаблонизаторы.

Добавляем новый движок

При добавлении нового движка мы создаем связку между самим движком и компонентом. Чтобы этого добиться надо реализовать три метода из интерфейса EngineInterface.

  • render($name, array $parameters = array()): этот метод отвечает за отрисовку шаблона, параметр nameсодержит имя шаблона, а parameters — переменные шаблона.
  • exists($name): проверяет существует ли файл шаблона
  • supports($name): возвращает значение true если связка движка и компонента может корректно обработать шаблон. Обычно на этом этапе проверяется проверка расширения файла. А также если существует несколько связок, то необходимо выбрать корректную.

Так что, если мы хотим подключить поддержку Twig нам всего лишь следует реализовать следующие методы:

<?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');
    }
}

Для того, чтобы мы могли использовать внедрение зависимостей в дальнейшем, в только что созданном экземпляре TwigEngine необходимо создать экземпляр Twig_Environment и объект реализующий интерфейс TemplateNameParserInterface, который выдаёт правильное название шаблонизатора основываясь на расширении файла. Так же этот объект принимает адрес каталога в качестве параметра.

Тоже самое касается любого шаблонизатора, поддержку которого мы хотим подключить. Например, класс для шаблонизатора Smarty выглядит следующим образом:

<?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');
    }
}

Пример использования в контроллере:

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

Метод render() обрабатывает все доступные движки и выполняет метод supports() для каждого из них. Как только найдет соответствующий движок, исполняется его метод render().

Замена PHP шаблонизатора: Plates

Если PHP для вас привычнее, но вам не хватает таких аспектов как наследование, базовые шаблоны, плагины и расширения, то есть смысл обратить внимание на движок Plates.

Чтобы подключить Plates в качестве движка по-умолчанию, надо создать новый класс реализующий EngineInterface и принимающий файлы с расширением php в качестве шаблонов.

<?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');
    }
}

Вызов setFileExtension() со значением параметра null необходим в нашем примере, чтобы использовать формат имени шаблона hello.php вместо hello.php.php.

Теперь просто заменим объект PhpEngine на PlatesEngine.

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

Сам шаблон не претерпел никаких изменений и теперь нам доступен весь функционал Plates.

Заключение

Компонент Templating сильно выручает, когда надо организовать поддержку нескольких движков, особенно во фреймворках, где активно используется шаблонизация, например статические генераторы блогов и системы плагинов.