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

    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 под забавным именем Твигрикс, он абсолютно бесплатен и доступен для скачивания всем интересующимся. А исходный код можно посмотреть на гитхабе. Мы от всей души надеемся, что Твигрикс немного украсит суровые будни суровых Битрикс-разработчиков.
    Метки:
    • +8
    • 11,1k
    • 3
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 3
    • +12
      github.com/HighTechnologiesCenter/twigrix/blob/master/.last_version/include.php#L2 используйте UTF8
      А почему используется старый твиг?
      global $arCustomTemplateEngines; после этого я не понимаю как можно использовать битрикс
      • 0
        >после этого я не понимаю как можно использовать битрикс
        Некоторые хотят денег сразу вместо того, чтобы поработать немного за бесценок, а потом, набравшись опыта, устроиться уже на нормальный проект.
        • 0
          используйте UTF8

          К сожалению, Bitrix Marketplace требует, чтобы исходники модуля поставлялись в кодировке Windows-1251

          А почему используется старый твиг?

          Используется версия, которая была актуальна в момент разработки модуля. Мы постараемся более-менее оперативно обновлять «зашитую» в модуль версию. Кроме того, думаем о том, как реализовать нормальное управление сторонними библиотеками в контексте Битрикс-проектов: классическое использование composer'a не всегда возможно, т.к. не на всех хостингах предоставляется достаточно прав для его запуска.

          после этого я не понимаю как можно использовать битрикс

          Использовать можно, зачастую он очень эффективно решает задачи бизнеса. Другое дело, что разработка под Битрикс далеко не всегда приносит удовольствие разработчикам, особенно привыкшим к современным моделям PHP-программирования.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.