Минутка самоиронии вместо дисклеймера:

Являясь битрикс-разработчиком, я всё же вынужден признать, что данная система, к сожалению, имеет свои изъяны, перечислять которые я, конечно же, не буду, ибо негоже кусать руку, которая тебя кормит

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

И вот в один прекрасный момент, попробовав Blade и Twig, я задумался о том, как бы какой-нибудь шаблонизатор к битре прикрутить.

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

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

В итоге, перелопатив (почти) всю информацию по этому поводу, я решил создать своё решение (почему никто не удивлён?). Сначала была идея запилить модуль, но потом решил использовать composer-пакет.

Это было небольшое вступление, теперь непосредственно к сути.

Регистрация расширения

В курсе "Разработчик Bitrix Framework" есть отдельный урок, описывающий подключение шаблонизатора. Как обычно, нужно использовать init.php - объявить глобальную переменную, зарегистрировать функцию обработчик. Но мы все знаем, к чему приводит привычка юзать init.php - в один прекрасный момент ты его открываешь и понимаешь, что так дальше жить нельзя. Собственно, поэтому и родилась идея вынести всю логику в отдельный класс (которая так же не является чем-то новым).

Я решил попробовать использовать статический метод нового класса вместо глобальной функции-обработчика, но оказалось, что в данном случае перестают работать теги и функции внутри шаблона. Также вскрылась проблема с добавлением кастомных расширений - не будут же разработчики править файлы в папке vendor.

В итоге взор был обращён в сторону событий, но т.к. опыта создания собственных событий у меня не было, то в силу собственной лени я решил не заморачиваться и взять компонент EventDispatcher от Symfony.

Однако совсем уходить от битриксовых событий я не стал, т.к. именно с их помощью и происходит та работа, которую все рекомендуют выносить в init.php.

Таким образом, в init.php всё обошлось двумя строчками кода:

require_once dirname(__DIR__, 2) . '/vendor/autoload.php';

StayFuneral\BitrixTwig\Template\Engine::register();

Данная функция регистрирует обработчик события OnPageStart:

$eventManager = EventManager::getInstance();
$eventManager->addEventHandler('main', 'OnPageStart', [TwigEvents::class, 'OnPageStart']);

А обработчик в свою очередь регистрирует ту самую глобальную переменную $arCustomTemplateEngines:

namespace StayFuneral\BitrixTwig\Events;

class TwigEvents
{
    public static function OnPageStart()
    {
        global $arCustomTemplateEngines;
        $arCustomTemplateEngines['twig'] = [
            'templateExt' => ['twig', 'html.twig'],
            'function' => 'renderTwigTemplate'
        ];
    }
}

Вывод на экран

В самой функции создаётся экземпляр диспетчера событий, который передаётся в основной класс библиотеки, и происходит отрисовка:

use StayFuneral\BitrixTwig\Template\Engine;
use Symfony\Component\EventDispatcher\EventDispatcher;

if(!function_exists('renderTwigTemplate')) {

    function renderTwigTemplate($templateFile, $arResult, $arParams, $arLangMessages, $templateFolder, $parentTemplateFolder, CBitrixComponentTemplate $template)
    {
        $dispatcher = new EventDispatcher();

        $engine = new Engine($dispatcher);
        $engine->addComponentEpilog($templateFolder, $template); // добавляется component_epilog.php

        echo $engine->render($templateFile, $arResult, $arParams, $arLangMessages, $templateFolder, $parentTemplateFolder, $template);
    }

}

Внутренности

Давайте посмотрим, что происходит под капотом основного класса

При инициализации в конструктор передаётся инстанс диспетчера событий, объявляются несколько важных объектов, и добавляются обработчики события:

		public function __construct(EventDispatcher $dispatcher)
    {
        $this->setRequest();
        $this->setLoader();
        $this->setTwig();

        $this->setDispatcher($dispatcher);
        $this->dispatchEvents();
    }

		protected function setRequest(): void
    {
        $this->request = Context::getCurrent()->getRequest();
    }

    protected function setLoader(): void
    {
        $this->loader = new FilesystemLoader(Application::getDocumentRoot());
    }

    protected function setTwig(): void
    {
        $this->twig = new Environment($this->loader, $this->getEnvOptions());
    }

Сам же метод render особой сложностью не блещет:

public function render($templateFile, $arResult, $arParams, $arLangMessages, $templateFolder, $parentTemplateFolder, CBitrixComponentTemplate $template): string
    {
        $this->twig->addExtension(new DefaultExtension());

        $renderOptions = $this->getRenderOptions($arResult, $arParams, $arLangMessages, $template, $templateFolder, $parentTemplateFolder);

        return $this->twig->render($templateFile, $renderOptions);
    }

Для удобства я вынес параметры в отдельные методы, поэтому если кому будет интересно, посмотрите потом на гитхабе.

Обработка событий

Отдельно стоит остановиться на диспетчере событий. Логично, что регистрировать расширения нужно до отрисовки шаблона, поэтому и событие было названо соответственно - twig.before_render

Ранее, как вы помните, мы добавили в базу таблицу twig_subscribers, в которую нужно добавлять подписчиков события, а также создали класс TwigRenderEvent, который это событие и генерирует:

namespace StayFuneral\BitrixTwig\Events;

use Symfony\Contracts\EventDispatcher\Event;
use Twig\Environment;

class TwigRenderEvent extends Event
{
    public const EVENT_NAME = 'twig.before_render';

    protected Environment $twig;

    public function __construct(Environment &$twig)
    {
        $this->twig = $twig;
    }

    /**
     * @return Environment
     */
    public function getTwig(): Environment
    {
        return $this->twig;
    }
}

Подписчик должен реализовывать интерфейс Symfony\Component\EventDispatcher\EventSubscriberInterface и содержать минимум 2 метода - статический getSubscribedEvents() и сам обработчик (в нашем случае onBeforeRenderTwig, принимающий на входе вышеупомянутый TwigRenderEvent) и добавляющий расширение:

namespace StayFuneral\Event;

use StayFuneral\BitrixTwig\Events\TwigRenderEvent;
use StayFuneral\Extensions\CustomExtension;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class TwigRenderSubscriber implements EventSubscriberInterface
{

    /**
     * @inheritDoc
     */
    public static function getSubscribedEvents()
    {
        return [
            TwigRenderEvent::EVENT_NAME => 'onBeforeRenderTwig'
        ];
    }

    /**
     * Добавление кастомных расширений в шаблон
     *
     * @param TwigRenderEvent $event
     */
    public function onBeforeRenderTwig(TwigRenderEvent $event)
    {
        $event->getTwig()->addExtension(new CustomExtension());
    }
}

Добавление расширений

Для того, чтобы добавить расширение в таблицу, нужно вызвать метод addSubscriber и передать в него наш класс:

use StayFuneral\BitrixTwig\Entites\TwigSubscribersTable;
use StayFuneral\Event\TwigRenderSubscriber;

TwigSubscribersTable::addSubscriber(TwigRenderSubscriber::class);

По умолчанию в библиотеке доступны пока только 2 расширения:

  • getMessage - то же самое, что Bitrix\Main\Localization\Loc::getMessage($phrase, $replace)

  • showComponent - вывод компонента, параметры и очерёдность аналогична основному методу

В итоге

А в итоге всё, что я только что описал, можно посмотреть на гитхабе или скачать в свой проект с помощью композера:

composer require stayfuneral/bitrix-twig-engine

В планах - написание модуля (для тех, кто хочет делать всё нажатием одной кнопки в админке), возможно ещё какие-то улучшения.

Ну и напоследок, как принято в научных кругах, список литературы, которая так или иначе повлияла на создание данной библиотеки:

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

PS: Специально для тех, кто хочет написать что-то про недостатки битрикса - пишите их в других местах, потому что, во-первых, они и так всем известны, а во-вторых, ваше мнение никому не интересно. Это же относится и ко всем любителям выискивать недостатки везде и во всём.

Комментарии (11)


  1. Divasoft
    28.09.2021 13:33
    +8

    А когда проект перейдёт по наследству - "ебитесь как хотите ещё и с другими фрэймфорками внутри фрэймворка", тут даже писать про "недостатки" не надо.


    1. FAT
      28.09.2021 16:51


    1. topuserman
      29.09.2021 17:44

      Где вы заметили яйцо в яйце?


      Symfony/EventDispatcher - фреймворк ?
      или может Twig - фреймворк ?

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


      1. rpsv
        30.09.2021 07:15

        Symfony/EventDispatcher - фреймворк ?

        Эвент диспетчер есть стандартный битриксовый, который вполне все потребности автора покрывает. Зачем нужно было тащить стороний?

        Twig - фреймворк

        Шаблонизатор. Битрикс по-умолчанию без него, и подавляющее большинство сайтов на Битрикс без шаблонизатора. И если этот чудо-сайт после уйдет кому-то другому на поддержку, то им весело и задорно придется разбираться в этом великолепии. Про документацию я думаю можно даже не мечтать в таких вот наследиях предыдущих разработчиков.


  1. rpsv
    28.09.2021 20:36

    Но мы все знаем, к чему приводит привычка юзать init.php

    Все мы это кто, говнокодеры? В init.php максимум, что нужно дак это вызывать 'Loader::includeModule', а все остальное уже размещать непосредственно в include.php модуля и/или доп файлах если include.php разрастается.

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

    А где еще вы их собираетесь использовать? Inline на публичных или админских страницах? Использование компонентов в целом хороший подход, когда у вас конкретный виджет (кусок кода) обособлен.

    но т.к. опыта создания собственных событий у меня не было, то в силу собственной лени я решил не заморачиваться

    Т.е. в целом вам лень написать 1 класс с двумя методами: 'addEventListener' которая добавит в массив колбэк и 'triggerEvent' которая прочитает из массива и выполнит колбэк?

    Да, действительно, лень невообразимая, а заморочек и подавно не видно.

    Хотя бы стоило добавить в конце листинг куска кода с использования Twig в компоненте


    1. topuserman
      30.09.2021 11:41
      -1

      Все мы это кто, говнокодеры? В init.php максимум, что нужно дак это вызывать 'Loader::includeModule', а все остальное уже размещать непосредственно в include.php модуля и/или доп файлах если include.php разрастается.

      Да-да, именно вынесения всей каши из init.php в отдельный файл и инклудить его обратно в init - это тру ?

      Пожалуйста, не учите людей хорошим практикам, не владея ими самим ?


      1. rpsv
        01.10.2021 08:00

        Да-да, именно вынесения всей каши из init.php в отдельный файл и инклудить его обратно в init - это тру ?

        Вы наверное сами не понимаете о чем говорите:

        # init.php
        <?php
        
        \Bitrix\Main\Loader::includeModule('module.name');
        
        ?>
        
        # module.name/include.php
        <?php
        
        // подписка на события
        // инциализация DI
        
        ?>

        И в чем тут проблема? А если модулей несколько, то наверное вместо очередного инклуда модуля (и группирования кода по контексту a.k.a модулю), лучше конечно выносить это все в init.php, естественно (сарказм).

        Ах да, наверное вы еще посоветуете не выносить обработчики событий в отдельный классы/функции, а прям в анонимной функции фигачить в init.php?

        Хотелось бы конечно посмотреть на ваши проекты с вашей "хорошей практикой". Говнище редкостное наверное :)


  1. udjin123
    30.09.2021 02:30
    -1

    От композита придётся отказаться, в таком случае. Но лучше отказаться от Битрикса и работать с symfony или laravel.


    1. Ramapriya Автор
      30.09.2021 03:06
      -1

      Для сайта согласен, хотя многие компании хотят бус, руководствуясь политикой импортозамещения.

      Но мы обычно разрабатываем для коробки Битрикс24, кроме того, не всем компонентам бывает нужен композит


      1. rpsv
        30.09.2021 07:17

        Конечно же импортозамещением. Не на маркетинг ведутся, ага.

        И на БУС и на Б24 можно поднимать адекватные проекты, если конечно ручки-не-криворучки, но видимо в этом случае как раз они.


      1. udjin123
        30.09.2021 10:52

        Я имел ввиду что разработчику лучше не заниматься битриксом, клиенты понятно что едят маркетинг и плюс преимущество в гос закупках у отечественного ПО. Но для развития это пагубно, располагает говнокодить и замыкает тебя только на внутренний рынок.