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

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 под забавным именем Твигрикс, он абсолютно бесплатен и доступен для скачивания всем интересующимся. А исходный код можно посмотреть на гитхабе. Мы от всей души надеемся, что Твигрикс немного украсит суровые будни суровых Битрикс-разработчиков.

© Habrahabr.ru