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

от автора

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

ссылка на оригинал статьи http://habrahabr.ru/post/220563/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *