Таки надоело мне лицезреть и постоянно рассказывать новичкам о том, что нельзя просто так взять, и долбить запросы к БД в шаблонах компонентах. А еще нельзя фигачить бизнес-логику туда же. Все, надоело … будем избавляться. Единственным правильным выходом из этой ситуации мне видится использование шаблонизатора.

Зачем нужен шаблонизатор? Как следует из его названия - для построения шаблонов 🙂 на самом деле все гораздо интереснее:
Во-первых, благодаря шаблонизатору, в шаблонах очень сильно усложняется процесс внедрения в него кода, которого там быть не должно.
Во-вторых, количество кода в шаблонах резко сокращается за счет отказа от php-шных конструкций.
В-третьих, и это самая киллер-фича, появляется возможность наследования шаблонов друг от друга! (правда не все шаблонизаторы обладают таким функционалом)

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

Почему, собственно, Twig

А собственно, хз 🙂 Вообще говоря, я поначалу приглядывался к Jade. Синтаксически очень красив, лаконичен. Но его реализация для php развивается достаточно медленно, поэтому пришлось отбросить этот вариант.

На заре своей карьеры сталкивался со Smarty, остались неприятные впечатления от него (скорее всего из-за недостатка опыта). Наверно, имеет смысл взглянуть на него по новой, но пока не до этого.

В последнее время набирает популярность Blade от команды Laravel, но мне его синтаксис кажется не очень привычным.

В пользу Twig был Symfony 2 и Drupal 8, плюс нативная поддержка его в phpStorm, несложное подключение и синтаксис, который быстро запоминается (что плюс для новичков).

Задача

Мне нужен модуль, который я смог бы с помощью composer по быстрому подрубить к проекту и использовать в битриксе. Быстрый гуглёж дал мне уже готовую реализацию модуля статью на хабре, в которой описывается процесс созидания такого модуля и подключения функционала шаблонизатора.
Однако, оно мне не подошло по нескольким причинам - его нельзя подцепить через composer, т.к. его исходники хранятся в виде обновлений для marketplace, причем в windows-1251 кодировке, наследование шаблонов там не реализовано в полной мере, а также в поставку модуля входит Twig, который нужно обновлять, в моем случае его будем выносить под контроль composer.

Делаем библиотеку

Сказано - сделано. От создания модуля для битрикс я сразу отказался, т.к. битрикс позволяет администратору сайта легко отрубать модуль, а в мои планы не входит возможность отключения этого функционала через админку. Поэтому просто сделаем composer библиотеку.

Давайте разбираться дальше. Первая засада нас подстерегает сразу же. Я уже писал в ранее, что битрикс реализовывает фичи «как то не до конца». Вот и шаблонизаторы он поддерживает, но только в компонентах 2.0. Шаблоны сайтов, статическое содержимое остаются не у дел. Ну что же - надо с чего то начинать, вот и начал подключать к компонентам.

В основе всей идеи интеграции шаблонизатора в битриксовые компоненты лежит создание глобальной функции (Битрикс, обожаю тебя!). Эта функция будет принимать на вход данные, с которыми работает шаблон (arResult, arParams, arLang, путь до файла шаблона и еще кое-что), и в результате работы этой функции она должна вывести в стандартный поток отрендеренное содержимое шаблона. При рендеринге шаблона битрикс проверяет, нет ли такой функции, и если она есть, то управление рендерингом передается ей.

Для начала нужно зарегистировать такую функцию. Сделать это можно с помощью глобального массива (снова обожаю тебя, Битрикс!) $arCustomTemplateEngines. Каждая функция рендеринга соответствует конкретному расширению файла. Делаем так, чтобы при рендеринге учитывалось расширение .twig

Регистрация выглядит следующим образом:

global $arCustomTemplateEngines;
$arCustomTemplateEngines['twig'] = [
    'templateExt' => ['twig'],
    'function'    => 'renderTwigTemplate'
];

Разместить этот код нужно где-то до того момента, как будут выполнены первые компоненты. Теперь нужно эту функцию реализовать.

function renderTwigTemplate(
    $templateFile,
    $arResult,
    $arParams,
    $arLangMessages,
    $templateFolder,
    $parentTemplateFolder,
    \CBitrixComponentTemplate $template)
{
    echo 'Наш шаблон';
}

Эту функцию нужно разместить там, где она будет доступна для вызова всеми модулями системы. В моем случае я использую автозагрузку composer для файлов. К сожалению, нельзя использовать callable-тип для функции, т.к. ядро проверяет именно наличие функции по ее имени. Исправление пары строчек в ядре позволило бы использовать callable и избавиться от этой глобальной функции, но, увы, не получится (по крайней мере пока).

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

Создаем класс, который будет хранить инстанс нашего Twig движка. Напомню, что Twig мы решили подключать через composer, поэтому примем во внимание, что если библиотека установлена, значит и ее зависимости уже тоже установлены, а автолоадер композера уже подключен. Таким образом, создаем класс:

namespace Maximaster\Twig;

class TemplateEngine
{
    private static $instance = null;

    private static function getInstance()
    {
        if (self::$instance) return self::$instance;
        \Twig_Autoloader::register();
        $loader = new \Twig_Loader_Filesystem($_SERVER['DOCUMENT_ROOT']);

        $cachePath = $_SERVER['DOCUMENT_ROOT'] . '/bitrix/cache/twig';
        $twig = new \Twig_Environment($loader, [
            'cache' => $cachePath,
            'charset' => SITE_CHARSET,
            'autoescape' => false
        ]);

        return self::$instance = $twig;
    }
}

Возможность получить инстанс наружу приложения мы не делаем специально, чтобы не засорять зависимостями наше приложение, а обращаться к готовому инстансу будем непосредственно из того же класса. По сути, нам нужен только метод, который будет вызываться из нашей суперглобальной функции renderTwigTemplate(), и получающий те же параметры.

Тут нас поджидает еще одна засада. Битрикс такой негодяй, не подключает файлы component_epilog при использовании стороннего шаблонизатора, поэтому приходится делать эту работу за него 🙁

/**
 * Собственно сама функция - рендерер. Принимает все данные о шаблоне и компоненте, выводит в stdout данные.
 * Содержит дополнительную обработку для component_epilog.php
 */
public static function render(
    $templateFile,
    $arResult,
    $arParams,
    $arLangMessages,
    $templateFolder,
    $parentTemplateFolder,
    $template
)
{
    if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) {
        throw new \Twig_Error('Пролог не подключен');
    }

    echo self::getInstance()->render($templateFile, array(
        'result' => $arResult,
        'params' => $arParams,
        'lang' => $arLangMessages,
        'template' => $template,
        'templateFolder' => $templateFolder,
        'parentTemplateFolder' => $parentTemplateFolder
    ));

    $component_epilog = $templateFolder . '/component_epilog.php';
    if(file_exists($_SERVER['DOCUMENT_ROOT'] . $component_epilog)) {
        $component = $template->__component;
        /** @var \CBitrixComponent $component */
        $component->SetTemplateEpilog(array(
            'epilogFile' => $component_epilog,
            'templateName' => $template->__name,
            'templateFile' => $template->__file,
            'templateFolder' => $template->__folder,
            'templateData' => false,
        ));
    }
}

Удобное наследование

Чтобы использовать наследование, нужно в шаблоне использовать управляющую конструкцию

{% extends "template_name" %}

где template_name - это некое имя шаблона. В него можно вписать полный или относительный путь до шаблона, от которого хотите унаследоваться, но это жутко неудобно, особенно в случае с битриксом (когда шаблон компонента с одним названием может храниться аж в 4х местах).

Чтобы побороть эту путаницу в Twig есть возможность создать свой загрузчик для шаблонов, чтобы обучать загружать шаблоны не по их пути, а по специальному имени. Для этого создан интерфейс \Twig_Loader_Interface, который нужно реализовать в виде класса и скормить его конструктору Twig_Environment.

Я решил использовать такой синтаксис, по-моему вполне удобно и удачно:

vendor:component_name[:template[:specific_template_file]

  • vendor - это пространство имен разработчика, например bitrix или maximaster
  • component_name - имя компонента, шаблон которого наследуется
  • template - имя шаблона, который нужно унаследовать. Необязательный, по-умолчанию .default
  • specific_template_file - конкретный файл шаблона (без расширения). Необязательный, по-умолчанию template

Например, вы хотите унаследовать шаблон new-year компонента maximaster:product. Для этого в шаблоне twig нужно написать

{% extends "maximaster:product:new-year" %}

Красота!

После этого мне понадобилось добавить возможность обращения к некоторым дефолтным функциям, переменным, константам битрикса и php, чтобы не потерять в функциональности. Для этого создаем расширение для Twig. Расширение создается также просто - нужно всего лишь создать класс наследник от Twig_Extension и зарегистрировать его при инициализации Twig_Environment.

Осталось добавить только пару штрихов по управлению конфигами и вуаля - библиотека готова, можно скачать на github.

Полный код TemplateEngine::getInstance получился таким:

private static function getInstance()
{
    if (self::$instance) return self::$instance;

    $loader = new BitrixLoader($_SERVER['DOCUMENT_ROOT']);

    $c = Configuration::getInstance();
    $config = $c->get('maximaster');
    $twigConfig = (array)$config['tools']['twig'];

    $defaultConfig = array(
        'debug' => false,
        'charset' => SITE_CHARSET,
        'cache' => $_SERVER['DOCUMENT_ROOT'] . '/bitrix/cache/maximaster/tools.twig',
        'auto_reload' => isset( $_GET[ 'clear_cache' ] ) && strtoupper($_GET[ 'clear_cache' ]) == 'Y',
        'autoescape' => false
    );

    $twigOptions = array_merge($defaultConfig, $twigConfig);

    $twig = new \Twig_Environment($loader, $twigOptions);

    if ($twig->isDebug())
    {
        $twig->addExtension(new \Twig_Extension_Debug());
    }

    $twig->addExtension(new BitrixExtension());

    return self::$instance = $twig;
}

Дальше будем думать, как подружить его с шаблонами сайтов и статикой.

Конечно, библиотека еще не готова до конца. Нужно продумать, например, какой-то механизм, который сможет автоматически очистить кеш при деплое, но мне думается, что это должен быть внешний механизм, не зависящий от этой библиотеки. Это расширение я включил в состав maximaster/tools, поэтому возможно в скором времени там и появится такой механизм. Время покажет.

Спасибо за внимание.