📜 ⬆️ ⬇️

Game patterns. How to reconcile Bitrix with third-party output template

I’ve been developing PHP for a long time, and during that time I learned to use the advantages of this language and to avoid, if possible, its drawbacks. But what I never liked about PHP is the built-in templating mechanism. The abundance of characters "<? Php ...?>" And verbose language constructs hits the eyes, the possibility of using arbitrary PHP code in the template does not contribute to the principle of separation of logic and presentation.

Therefore, I am grateful to the fate (and the developer community, of course) for the fact that there are alternative templating engines, with a much more pleasant syntax with the same functionality. Well, since most of the PHP projects in our Center for High Technologies are developed on the Symfony2 Framework, Twig has become our favorite templating engine. In addition to the above advantages, it is also infinitely expandable, which very often helps in the work.

But life often surprises. So, a small but quite interesting project that I needed to do on ... Bitrix has recently fallen on me! Fortunately, I already had to work with Bitrix, but it was a long time ago (and not true) , so I took the project as an opportunity to look at my past experience from a new perspective, to apply accumulated knowledge and skills in a slightly different context.
And the first thing I wanted to do was twist Twig, so as not to suffer with native templating.
')
That's what came out of it.

Fortunately, Bitrix allows you to use any output template engine. True, only for component templates, site templates are still created in PHP. To connect the template engine, you must declare a global function (yes, yes, this is Bitrix, baby), which will render the template. A function might look like this:

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, )); } 


In addition, the function needs to be registered in the global $arCustomTemplateEngines with the indication of the template file extension:

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


As a result, if a file named template.twig is in the component template directory, the renderTwigTemplate() rendering function will be called, and all necessary data will be transferred to the input: the name and path to the template file, component call parameters, component execution result, and language constants for this template.
As it turned out, there is one unpleasant feature: if the template.twig and template.php files are in the component template directory at the same time, then the PHP template will be used. Therefore, to implement a beautiful implicit substitution of the type of templates when connecting / disconnecting one or another template will not work.

After the rendering function is registered, it remains to initialize and configure the engine itself. In the case of Twig, you need to connect its autoloader to the project, specify the path to the template directory and set configuration parameters (the most important of them are using the debug mode and the way to store the template cache). Also, if necessary, you can add the necessary extensions. All this might look like this:

 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(); } } 


The use of static methods and class properties in this case is due to the Bitrix architecture: there is no mechanism for placing service objects like, for example, the service container from Symfony2.

The initialization work of the template engine is performed in the initialize () method. I note that in our case the Twig connection is encapsulated in a separate Bitrix module. This, firstly, gave us the opportunity to conveniently use the functionality on different projects, and secondly, it allowed us to set some configuration parameters through the CMS administrative interface. In particular, the debugging mode is enabled depending on the value of the debug_mode option, which is controlled on the module settings page in the Bitrix admin area.
Since we are talking about configuration parameters, I allow myself to make a small lyrical digression. The working principle of Twig is as follows: the first time a template is accessed, it is compiled into PHP code, which is then executed on all subsequent references. The files with the generated code are called the template cache and are placed in the directory specified in the cache option. When changing the template source code, of course, the cache should be disabled. The easiest way that is usually used when a new functionality is released is a complete clearing of the cache directory, which is implemented by calling the Twig_Environment::clearCacheFiles() method (our module has a wrapper for this method that allows you to clear the cache by pressing a button in the administrative interface). In addition, Twig can automatically recreate the cache of a particular template when its source code changes: to do this, set the auto_reload option to true. But usually this approach is required only in development mode, so instead of auto_reload you can set the debug option, which will have the same effect when working with the cache, and also allow you to use the Twig debugging features.
By the way, the Twig template cache is not connected in any way and does not conflict with the Bitrix template cache, since in the first case the PHP code is cached, and in the second, the data obtained as a result of the component’s operation and the HTML markup.
In the context of Bitrix, it also proved important to set the autoescape option to false, since already shielded data is passed to the rendering function.

The initialization method is called in the module connection file:

 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); 


As can be seen from this code, the path to the cache directory can also be configured on the module settings page.

So, the template engine is registered and configured, it's time to start using it. And here, as usual, it was not without pitfalls.
First, Bitrix components often have to use certain Bitrix functions as well as global objects (what to do, the costs of the CMS architecture). Fortunately, Twig, as I have already noted, allows you to create your own extensions in which you can describe additional tags, filters, functions, etc. Therefore, a BitrixTwigExtension was developed, providing access to the Bitrix API in templates. At the same time, we tried to keep the minimum set of APIs accessible in order to protect developers from the desire to implement business logic in templates.
Then, after long attempts to understand why the language constants are not transferred to the template, and the subsequent study of the CMS core code, it became clear that the language template file should have the exact same name as the template itself, including the extension. This means that the language template file template.twig should also be named template.twig , while remaining a PHP file! Well, strange behavior, but as it turned out, one could not even expect anything like this from Beatrix developers.
The most annoying thing was that, when using Twig templates, I did not work out component_epilog (the final stage of rendering the template in Bitrix, which allows you to perform any actions, regardless of whether the template is cached or not). Again, learning kernel code - and another amazement: component_epilog connects only to native templates! More controversial decisions in Bitrix, I still, perhaps, have not met. The only available way to correct this situation is to manually call component_epilog after rendering the template:

 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, )); } } 


After the improvements, we finally got a really usable solution that made life easier for me (the project that started everything was successfully implemented) and my colleagues, who also liked the simplicity and brevity of Twig.
And, of course, we could not but share the result of our labors. The module is located in the Bitrix Marketplace under the funny name Twigrix , it is absolutely free and available for download to all those interested. And the source code can be viewed on github . We sincerely hope that Twigriks will slightly decorate the harsh everyday life of the harsh Bitrix developers.

Source: https://habr.com/ru/post/220563/


All Articles