23 April 2014

Игра шаблонов. Как примирить Битрикс со сторонним шаблонизатором вывода

PHP1С-Bitrix
PHP-разработкой я занимаюсь уже довольно давно, и за это время научился использовать преимущества этого языка и избегать, по возможности, его недостатков. Но что мне никогда не нравилось в PHP — это встроенный механизм шаблонизации. Обилие символов “<?php … ?>” и многословных языковых конструкций бьет по глазам, возможность использования в шаблоне произвольного PHP-кода не способствует соблюдению принципа разделения логики и представления.

Поэтому я благодарен судьбе (и сообществу разработчиков, конечно) за то, что существуют альтернативные движки шаблонизации, с гораздо более приятным синтаксисом при тех же функциональных возможностях. Ну, а поскольку большая часть PHP-проектов у нас, в Центре Высоких Технологий, разрабатывается на Symfony2 Framework, то нашим любимым шаблонизатором стал Twig. Помимо указанных выше преимуществ, он еще и безгранично расширяемый, что очень часто помогает в работе.

Но жизнь частенько преподносит сюрпризы. Вот и на меня недавно свалился небольшой, но довольно интересный проект, делать который нужно было на… Битриксе! К счастью, работать с Битриксом мне уже приходилось, но было это давно (и неправда), поэтому я воспринял проект как возможность посмотреть на свой прошлый опыт с новой точки зрения, применить накопленные знания и навыки в несколько ином контексте.
И первое, что мне захотелось сделать — “прикрутить” Twig, чтобы не мучиться с нативной шаблонизацией.

Вот что из этого получилось.

К счастью, Битрикс позволяет использовать любой шаблонизатор вывода. Правда, только для шаблонов компонентов, шаблоны сайта все равно создаются на PHP. Для подключения шаблонизатора необходимо объявить глобальную функцию (да-да, это Битрикс, детка), которая будет осуществлять рендеринг шаблона. Функция может выглядеть, например, так:

function renderTwigTemplate($templateFile, $arResult, $arParams, $arLangMessages, $templateFolder, $parentTemplateFolder, $template)
{
    echo TwigTemplateEngine::renderTemplate($templateFile, array(
        'params' => $arParams,
        'result' => $arResult,
        'langMessages' => $arLangMessages,
        'template' => $template,
        'templateFolder' => $templateFolder,
        'parentTemplateFolder' => $parentTemplateFolder,
    ));
}


Кроме того, функцию требуется зарегистрировать в глобальном массиве $arCustomTemplateEngines с указанием расширения файла шаблона:

global $arCustomTemplateEngines;
$arCustomTemplateEngines["twig"] = array(
    "templateExt" => array("twig"),
    "function"    => "renderTwigTemplate"
);


В результате, если в каталоге шаблона компонента находится файл с именем template.twig, будет вызвана функция рендеринга renderTwigTemplate(), на вход которой будут переданы все необходимые данные: имя и путь к файлу шаблона, параметры вызова компонента, результат выполнения компонента, а также языковые константы для данного шаблона.
Как выяснилось, есть одна неприятная особенность: если в каталоге шаблона компонента одновременно находятся файлы template.twig и template.php, то использоваться будет PHP-шный шаблон. Следовательно, реализовать красивую неявную подмену типа шаблонов при подключении/отключении того или иного шаблонизатора не получится.

После того, как функция рендеринга зарегистрирована, остается проинициализировать и настроить сам движок. В случае Twig необходимо подключить к проекту его autoloader, указать путь к каталогу шаблонов и задать конфигурационные параметры (наиболее важные из них — использование отладочного режима и способ хранения кэша шаблонов). Также, при необходимости, можно добавить нужные расширения. Все это может выглядеть следующим образом:

class TwigTemplateEngine
{
    private static $twigEnvironment;

    public static function initialize($templateRootPath, $cacheStoragePath)
    {
        Twig_Autoloader::register();

        $debugModeOptionValue = COption::GetOptionString("htc.twigintegrationmodule", "debug_mode");
        $debugMode = ($debugModeOptionValue == "Y") ? true : false;

        $loader = new Twig_Loader_Filesystem($templateRootPath);
        self::$twigEnvironment = new Twig_Environment($loader, array(
            'autoescape' => false,
            'cache'      => $cacheStoragePath,
            'debug'      => $debugMode
        ));


        self::addExtensions();

        global $arCustomTemplateEngines;
        $arCustomTemplateEngines["twig"] = array(
            "templateExt" => array("twig"),
            "function"    => "renderTwigTemplate"
        );
    }

    private static function addExtensions()
    {
        self::$twigEnvironment->addExtension(new Twig_Extension_Debug());
        self::$twigEnvironment->addExtension(new BitrixTwigExtension());
    }

    public static function renderTemplate($templateFile, array $context)
    {
        return self::$twigEnvironment->render($templateFile, $context);
    }

    public static function clearCacheFiles()
    {
        self::$twigEnvironment->clearCacheFiles();
    }
}



Использование статичных методов и свойств класса в данном случае обусловлено архитектурой Битрикса: в нем нет механизма для размещения сервисных объектов, подобного, к примеру, контейнеру сервисов из Symfony2.

Работа по инициализации шаблонизатора выполняется в методе initialize(). Отмечу, что в нашем случае подключение Twig инкапсулировано в отдельном модуле Битрикса. Это, во-первых, дало нам возможность удобного использования функционала на разных проектах, а во-вторых, позволило задавать некоторые конфигурационные параметры через административный интерфейс CMS. В частности, отладочный режим включается в зависимости от значения опции debug_mode, управление которой вынесено на страницу настроек модуля в админке Битрикса.
Поскольку речь зашла о конфигурационных параметрах, то позволю себе сделать небольшое лирическое отступление. Принцип работы Twig заключается в следующем: при первом обращении к шаблону он компилируется в PHP-код, который затем исполняется при всех последующих обращениях. Файлы со сгенерированным кодом называются кэшем шаблонов и помещаются в каталог, указанный в опции cache. При изменении исходного кода шаблона, естественно, кэш нужно инвалидировать. Самый простой способ, который обычно применяется при релизе нового функционала — это полная очистка каталога кэша, которая реалиуется вызовом метода Twig_Environment::clearCacheFiles() (в нашем модуле реализована обертка для этого метода, позволяющая очищать кэш по нажатию кнопки в административном интерфейсе). Кроме того, Twig умеет автоматически пересоздавать кэш конкретного шаблона при изменении его исходного кода: для этого необходимо установить опцию auto_reload в значение true. Но обычно такой подход требуется только в режиме разработки, поэтому вместо auto_reload можно установить опцию debug, что даст такой же эффект при работе с кэшем, а также позволит использовать отладочные возможности Twig.
Кстати, кэш шаблонов Twig никак не связан и не конфликтует с кэшем шаблонов Битрикса, поскольку в первом случае кэшируется PHP-код, а во втором — данные, полученные в результате работы компонента и HTML-разметка.
В контексте Битрикса также оказалось важным установить опцию autoescape в значение false, так как в функцию рендеринга передаются уже экранированные данные.

Вызов метода инициализации выполняется в файле подключения модуля:

CModule::AddAutoloadClasses(
    'htc.twigintegrationmodule',
    array(
        'TwigTemplateEngine' => 'classes/general/templating/TwigTemplateEngine.php',
        'BitrixTwigExtension' => 'classes/general/templating/BitrixTwigExtension.php',
        'Twig_Autoloader' => 'vendor/Twig/Autoloader.php',
    )
);

// Initialize Twig template engine
$documentRoot = $_SERVER['DOCUMENT_ROOT'];
$cacheStoragePathOption = COption::GetOptionString("htc.twigintegrationmodule", "cache_storage_path");

if ($cacheStoragePathOption == "") {
    $cacheStoragePath = $documentRoot . BX_PERSONAL_ROOT . "/cache/twig";
} else {
    $cacheStoragePath = $documentRoot . $cacheStoragePathOption;
}

TwigTemplateEngine::initialize($documentRoot, $cacheStoragePath);


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

Итак, шаблонизатор зарегистрирован и настроен, самое время начинать им пользоваться. И здесь, как обычно, не обошлось без подводных камней.
Во-первых, зачастую в шаблонах компонентов Битрикса приходится использовать некоторые битриксовые функции, а также глобальные объекты (что поделать, издержки архитектуры CMS). К счастью, Twig, как я уже отмечал, позволяет создавать собственные расширения, в которых можно описывать дополнительные теги, фильтры, функции и т.д. Поэтому было разработано небольшое расширение BitrixTwigExtension, предоставляющее доступ к API Битрикса в шаблонах. При этом мы постарались оставить доступным минимальный набор API, чтобы оградить разработчиков от желания реализовывать бизнес-логику в шаблонах.
Затем, после долгих попыток понять, почему же в шаблон не передаются языковые константы, и последующего изучения кода ядра CMS, стало ясно, что языковой файл шаблона должен иметь точно такое же имя, что и сам шаблон, включая расширение. Это означает, что языковой файл шаблона template.twig должен также иметь имя template.twig, оставаясь при этом PHP-файлом! Что ж, странное поведение, но, как выяснилось, от разработчиков Битрикса можно еще и не такого ожидать.
Самым неприятным стало то, что при использовании Twig-шаблонов не отрабатывал component_epilog (завершающий этап рендеринга шаблона в Битриксе, позволяющий выполнить какие-либо действия независимо от того, закеширован шаблон или нет). Опять изучение кода ядра — и очередное изумление: component_epilog подключается только к нативным шаблонам! Более спорного решения в Битриксе, я еще, пожалуй, не встречал. Единственный доступный способ исправления данной ситуации — вручную вызывать component_epilog после рендеринга шаблона:

function renderTwigTemplate($templateFile, $arResult, $arParams, $arLangMessages, $templateFolder, $parentTemplateFolder, $template)
{
    echo TwigTemplateEngine::renderTemplate($templateFile, array(
        'params' => $arParams,
        'result' => $arResult,
        'langMessages' => $arLangMessages,
        'template' => $template,
        'templateFolder' => $templateFolder,
        'parentTemplateFolder' => $parentTemplateFolder,
    ));

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



После проведенных доработок мы, наконец, получили действительно пригодное к использованию решение, которое упростило жизнь и мне (тот проект, с которого все и началось, был успешно реализован), и моим коллегам, которым тоже понравилась простота и лаконичность Twig.
И, конечно, мы не могли не поделиться результатом своих трудов. Модуль размещен в Bitrix Marketplace под забавным именем Твигрикс, он абсолютно бесплатен и доступен для скачивания всем интересующимся. А исходный код можно посмотреть на гитхабе. Мы от всей души надеемся, что Твигрикс немного украсит суровые будни суровых Битрикс-разработчиков.
Tags:bitrixtwigphp
Hubs: PHP 1С-Bitrix
+8
12.8k 60
Comments 3