MODX Revolution зустрічає Fenom

Останнім часом в англомовному співтоваристві MODX багато міркувань на тему «як нам жити далі». Всі навперебій обговорюють майбутню (через кілька років, вважаю) мажорну версію 3, а ми поки покращуємо своїми доповненнями поточну.

Свіже подія, якою я б хотів поділитися з широкою аудиторією — це випуск нової версії pdoTools шаблонизатором Fenom, яка дозволить вам повністю позбутися від нагромадження тегів в умовах чанків і переписати їх на простій і зрозумілій мові шаблонизатора.

Процедура не вимагає змін в роботі сайту, просто оновіть pdoTools до версії 2.0 і новий синтаксис. Найприємніше, що теги MODX чудово сусідять з Fenom і працюють разом без будь-яких проблем. Простий приклад для затравки:
{if $parent == 3}
[[!pdoMenu?parents=`0`]]
{else}
[[!pdoResources?parents=`1,2,3`]]
{/if}
Під катом величезну кількість інформації про парсере pdoTools, яку я ще жодного разу не збирав в одному місці.

Отже, парсер pdoTools представляє із себе окремий клас, який прописується в системних налаштуваннях MODX і перехоплює обробку тегів на сторінці.
В старих версіях компонента, включення парсера потрібно було підтверджувати при установці, але з версії 2.1.1-pl він включається за замовчуванням. Якщо, з якихось причин, вас це не влаштовує — видаліть системні налаштування
  • parser_class — ім'я класу парсера
  • parser_class_path — шлях до класу парсера
За замовчуванням в MODX немає цих налаштувань, вони потрібні тільки для підключення стороннього парсера, як у нашому випадку.

Принцип роботи
pdoParser може бути використаний у двох випадках:
  • при рендері чанка фрагментів — це відбувається завжди і у всіх фрагментах, що використовують pdoTools, незалежно від системних налаштувань.
  • при рендері сторінки — тільки якщо парсер включений в налаштуваннях системи.


Обробка чанка
У класі pdoTools для цього є 2 методу, дуже схожих на такі в класі modX:

  • getChunk — повна обробка чанка, може задіяти рідний парсер MODX
  • parseChunk — тільки заміна плейсхолдеров на значення, modParser не викликається
Основною особливістю роботи цих методів є те, що для завантаження чанка використовується protected метод _loadChunk, який може не тільки завантажувати чанк з бази даних, але і перетворює в нього довільні рядки.

Варіанти чанків
Отже, обидва методу pdoTools підтримують наступні види імен чанків:

@INLINE або@CODE

Один з найпопулярніших варіантів — вказівка тіла чанка прямо на сторінці. Наприклад:
[[!pdoResources?
&parents=`0`
&tpl=`@INLINE<p>{{+id}} - {{+pagetitle}}</p>`
]]

У такому вказівці є особливість, про яку багато людей не замислюються — все плейсхолдеры всередині чанка будуть оброблені парсером до виклику фрагменту.

Тобто, якщо викликати сніппет на сторінку:
[[!pdoResources?
&parents=`0`
&tpl=`@INLINE<p>[[+id]] - [[+pagetitle]]</p>`
]]

і в пам'яті системи при цьому виставлені плейсхолдеры
[[+id]]
або
[[+pagetitle]]
, то в сніппет прийде вже оброблений чанк і ви отримаєте на сторінку однакові рядки, типу:
15 - тест
15 - тест
15 - тест

Просто однакові значення, які виставив якийсь інший фрагмент раніше. Саме тому в прикладі у нас такі незвичайні плейсхолдеры —
{{+}}
замість
[[+]]
. Системний аналізатор їх не чіпає, а pdoTools замінює їх на нормальні під час роботи.

Ви можете використовувати фігурні дужки в якості обрамлення плейсхолдеров у всіх чанках pdoTools — він сам перетворить їх в
[[+]]
при завантаженні.

З цієї ж причини у вас ніколи не будуть працювати виклики фрагментів та фільтрів в INLINE чанках. Ось так працювати не буде:
[[!pdoResources?
&parents=`0`
&tpl=`@INLINE<p>[[+id]] - [[+pagetitle:default=`назва сторінки`]]</p>`
]]

А ось так — без проблем
[[!pdoResources?
&parents=`0`
&tpl=`@INLINE<p>{{+id}} - {{+pagetitle:default=` назва сторінки`}}</p>`
]]

Пам'ятайте про цей нюанс при використанні INLINE чанків.

@FILE

Багато людей звинувачують MODX в тому, що він не вміє зберігати чанкі у файлах і змушує зайвий раз працювати з базою даних. Це і незручно для системи контролю версій, і повільніше.

З версії 2.2 MODX пропонує використовувати для цих цілей статичні елементи, але по ряду причин, цей спосіб все одно може бути менш зручний, ніж пряма робота з файлами.

pdoTools відкриває таку можливість при вказівці @FILE:
[[!pdoResources?
&parents=`0`
&tpl=`@resources FILE/mychank.tpl`
]]

В цілях безпеки, використовувати можна тільки файли з расширеним html і tpl, і тільки з певної, наперед заданій директорії. За замовчуванням це:
/assets/elements/chunks/
.

Ви можете вказати свою власну директорію для файлів через параметр
&tplPath
:
[[!pdoResources?
&parents=`0`
&tpl=`@resources FILE/mychunk.tpl`
&tplPath=`/core/elements`
]]

Файл буде завантажений з файлу
/core/elements/resources/mychunk.tpl
від кореня сайту.

@TEMPLATE

Цей тип чанка дозволяє використовувати шаблони системи (тобто об'єкти modTemplate) для оформлення висновку.
[[!pdoResources?
&parents=`0`
&tpl=`@TEMPLATE Base Template`
]]

Якщо вказано порожній шаблон і в цих записах є поле
template
з id ім'ям або шаблону, то запис буде обгорнута в цей шаблон:
[[!pdoResources?
&parents=`0`
&tpl=`@TEMPLATE`
]]

Це такий аналог фрагменту renderResources.

При виведенні шаблону можна вказувати і набір параметрів (як у фрагментів):
[[!pdoResources?
&parents=`0`
&tpl=`@TEMPLATE Base Template@MyPropertySet`
]]

Тоді значення з цього набору будуть вставлені в шаблон.

Звичайні чанкі

Це режим за замовчуванням, який завантажує чанк з бази даних:
[[!pdoResources?
&parents=`0`
&tpl=`MyChunk`
]]

Точно так само підтримуються і набори параметрів:
[[!pdoResources?
&parents=`0`
&tpl=`MyChunk@MyPropertySet`
]]

Ці способи завантаження чанків працюють в усіх рідних фрагментах pdoTools і в усіх інших, які використовують методи pdoTools
getChunk
та
parseChunk
.

Метод getChunk
Оголошення цього методу виглядає так:
getChunk(string $chunkName, array $properties, bool $fastMode = false

Метод завантажує вказаний чанк (слідуючи вказівкою @BINDING, якщо є) і повністю обратывает його, замінюючи всі плейсхолдеры на передані значення (параметр $properties).

Третій параметр
fastMode
вирізає всі необроблені плейсхолдеры, щоб не було зайвих тегів на сторінці. Якщо цього не зробити, то парсер буде намагатися рекурсивно розібрати ці теги (до 10 ітерацій), що може привести до уповільнення роботи.

Рекурсивний аналізатор — це одне з достоїнств MODX і спеціально залишені теги дуже часто зустрічаються в логіці роботи фрагментів системи. Тому
fastMode
відключений за замовчуванням і використовувати його потрібно, тільки якщо ви впевнені в тому, що робите.

Парсер pdoTools не буде викликати системний аналізатор, якщо зміг самостійно розібрати всі плейсхолдеры. Якщо ж у чанке залишилися якісь виклики фільтрів або фрагмента, то робота передається в modParser, що вимагає додатковий час на обробку.

Метод parseChunk
А цей метод оголошено ось так:
parseChunk(string $name, array $properties, string $prefix= '[[+', string $suffix= ']]')

Він також створює чанк із зазначеного імені, розбираючи @BINDING, якщо є, а потім просто замінює плейсхолдеры на значення, без особливих обробок.

Це самий простий і швидкий спосіб оформлення даних в чанкі.

Обробка сторінки
Якщо pdoParser включений в налаштуваннях, то він викликається і для обробки всієї сторінки при виведенні її користувачеві.

При використанні цього парсера всі чанкі і доповнення MODX обробляються трохи швидше. Лише "трохи" тому, що він не бере на себе умови і фільтри, обробляючи тільки простенькі теги типу
[[+id]]
[[~15]]
. Однак, він це робить швидше modParser, тому що не створює зайвих об'єктів.

Крім можливої надбавки швидкості, ви отримуєте ще й нові можливості зручному висновку даних з різних ресурсів.

Теги fastField
В кінці 2012 року громадськості був представлений невеликий плагін з додаванням нових тегів парсеру MODX, який потім виріс в компонент fastField.

Він додає в систему опрацювання додаткових плейсхолдеров, типу
[[#15.pagetitle]]
. З дозволу автора, цей функціонал вже включений в pdoParser, і навіть трохи розширений.

Всі теги fastField починаються з
#
і далі містять або id потрібного ресурсу, або назву глобального масиву.

Висновок звичайних полів ресурсів:
[[#15.pagetitle]]
[[#20.content]]

ТБ параметри ресурсів:
[[#15.date]]
[[#20.some_tv]]

Поля товарів miniShop2:
[[#21.price]]
[[#22.article]]

Масиви ресурсів і товарів:
[[#12.properties.somefield]]
[[#15.size.1]]

Суперглобальні масиви:
[[#POST.key]]
[[#SESSION.another_key]]
[[#GET.key3]]
[[#REQUEST.key]]
[[#SERVER.key]]
[[#FILES.key]]
[[#COOKIE.some_key]]

Можна вказувати будь-які поля в масивах:
[[#15.properties.key1.key2]]

Якщо ви не знаєте, які значення знаходяться всередині масиву — просто вкажіть його і він буде роздрукований повністю:
[[#GET]]
[[#15.colors]]
[[#12.properties]]

Теги fastField можна поєднувати з тегами MODX:
[[#[[++site_start]].pagetitle]]

[[#[[++site_start]]]]

Шаблонизатор Fenom
Підтримка шаблонизатора Fenom з'явилася в pdoTools з версії 2.0, після чого він став вимагати PHP 5.3+.

Він працює набагато швидше, ніж рідний modParser, і якщо ви перепишіть свій чанк так, що в ньому не буде жодного тега MODX, то modParser і зовсім не буде запускатися. При цьому, звичайно, одночасна робота і старих тегів, і нових в одному чанке допускається.

На обробку шаблонизатором впливають наступні системні параметри:
  • pdotools_fenom_default — включає обробку через Fenom чанків pdoTools. Включено за замовчуванням.
  • pdotools_fenom_parser — включає обробку шаблонизатором всіх сторінок сайту. Тобто, не тільки чанків, але і шаблонів.
  • pdotools_fenom_php — включає підтримку PHP функцій в шаблонизаторе. Дуже небезпечна функція, так як будь-менеджер отримає доступ до PHP прямо з чанка.
  • pdotools_fenom_modx — додає системні змінні
    {$modx}
    {$pdoTools}
    шаблони Fenom. Теж дуже небезпечно — будь-який менеджер може управляти об'єктами MODX з чанків.
  • pdotools_fenom_options — JSON рядок з масивом налаштувань згідно офіційній документації. Наприклад:
    {"auto_escape":true,"force_include":true}
  • pdotools_fenom_cache — кешування скопмилированных шаблонів. Має сенс тільки для складних чанків на робочих сайтах, вимкнуто за промовчанням.
Отже, за замовчуванням Fenom включений для роботи тільки в чанках, які проходять через pdoTools. Це цілком безпечно і менеджери системи не отримують жодних додаткових можливостей, крім більш зручного синтаксису і високої швидкості роботи.

Включення pdotools_fenom_parser дозволяє використовувати синтаксис Fenom прямо у контенті документів та шаблони сторінок, але є один нюанс — шаблонизатор може неправильно реагувати на фігурні дужки, які в MODX дуже люблять.

У таких випадках автор рекомендує використовувати тег {ignore}.

Якщо ви плануєте включити Fenom глобально для всього сайту, вам слід перевірити, чи на всіх сторінках він нормально працює.

Синтаксис

Для початку раджу прочитати офіційну документацію, а далі ми розглянемо синтаксис стосовно MODX.

Всі змінні від фрагментів передаються в чанк як є, тому переписувати старі чанкі на новий синтаксис — суцільне задоволення.





MODX Fenom [[+id]] {$id} [[+id:default=`test`]] {$id ?: 'test'} [[+id:is=`:then=`test`:else=`[[+pagetitle]]`]] {$id == "? 'test': $pagetitle}
Для використання більш складних сутностей, pdoParser передбачена службова змінна{$_modx}, яка дає безпечний доступ до деяких змінних та методів системи.








MODX Fenom [[*id]] {$_modx->resource.id} [[*tv_param]] {$_modx->resource.tv_param} [[%lexicon]] {$_modx->lexicon('lexicon')} [[~15]] {$_modx->makeUrl(15)} [[~[[*id]]]] {$_modx->makeUrl($_modx->resource.id)} [[++system_setting]] {$_modx->config.system_setting}
Крім цього вам доступні змінні:
{$_modx->config} — системні налаштування
{$_modx->config.site_name}
{$_modx->config.emailsender}
{$_modx->config['site_url']}
{$_modx->config['any_system_setting']}

{$_modx->user} — масив поточного користувача. Якщо він авторизований, то додаються і дані з профілю:
{if $_modx->user.id > 0}
Привіт,{$_modx->user.fullname}!
{else}
Вам потрібно авторизуватися.
{/if}

{$_modx->context} — масив з поточним контекстом
Ви знаходитесь в контексті{$_modx->context.key}

{$_modx->resource} — масив з поточним ресурсом, це ви вже бачили в прикладах вище
{$_modx->resource.id}
{$_modx->resource.pagetitle}
{$_modx->makeUrl($_modx->resource.id)}

{$_modx->lexicon} — об'єкт (не масив!) modLexicon, який можна використовувати для завантаження довільних словників:
{$_modx->lexicon->load('ms2gallery:default')}
Перевірка словників ms2Gallery:{$_modx->lexicon('ms2gallery_err_gallery_exists')}

За виведення записів відповідає окрема функція
{$_modx->lexicon()}
.

Плейсхолдеры з точкою

Fenom використовує точку для доступу до значення масиву, а MODX зазвичай выствляет так плейсхолдеры з масивів. Відповідно, для тегів [[+tag.sub_tag]] аналогів в Fenom не передбачено.

Тому для подібних плейсхолдеров вам необхідно використовувати другу службову змінну — {$_pls}:
{$_pls['tag.subtag']}

Висновок фрагментів і чанків

Змінна
{$_modx}
насправді являє собою простий і безпечний клас microMODX

Тому текстові фрагменти і чанкі викликаються так:
{$_modx->runSnippet('!pdoPage@PropertySet', [
'parents' => 0,
'showLog' => 1,
'element' => 'psoResources',
'where' => ['isfolder' => 1],
'showLog' => 1,
])}
{$_modx->getPlaceholder('page.total')}
{$_modx->getPlaceholder('page.nav')}

Як бачите, синтаксис практично повністю повторює PHP, що відкриває нові можливості. Наприклад, можна вказувати масиви, замість JSON рядків.

За замовчуванням усі фрагменти викликаються кэшированными, але ви можете додати
!
перед іменем — як в тегах MODX.

Якщо для виклику фрагменту використовується рідний метод MODX, то для виведення чанків запускається pdoTools, і ви можете використовувати всі його можливості:
{$_modx->getChunk('MyChunk@PropertySet')}

{$_modx->parseChunk('MyChunk', [
'pl1' => 'placeholder1',
'pl2' => 'placeholder2',
])}

{$_modx->getChunk('@TEMPLATE Base Template')}

{$_modx->getChunk('@INLINE
Ім'я сайту:{$_modx->config.site_name}
')}

{$_modx->getChunk(
'@INLINE Передача перемнной в чанк: {$var}',
['var' => 'Тест']
)}

{$_modx->getChunk('
@INLINE Передача змінної виклик фрагменту:
{$_modx->runSnippet("pdoResources", [
"parents" => $parents
])}
Усього результатів:{$_modx->getPlaceholder("total")}
',
['parents' => 0]
)}

Приклади вище трохи божевільні, але цілком собі працюють.

Керування кешуванням

В об'єкті{$_modx} доступний сервіс modX::cacheManager, який дозволяє вам встановлювати довільний час кешування викликаються фрагментів:
{if !$snippet = $_modx->cacheManager->get('cache_key')}
{set $snippet = $_modx->runSnippet('!pdoResources', [
'parents' => 0,
'tpl' => '@INLINE {$id} - {$pagetitle}',
'showLog' => 1,
])}
{set $null = $_modx->cacheManager->set('cache_key', $snippet, 1800)}
{/if}

{$snippet}

Подивитися цей кеш можна в
/core/cache/default/
у прикладі він зберігається на 30 хвилин.

set $null = ...
потрібен, щоб
cacheManager->set
не вивів 1 (тобто true) на сторінку.

А ще ви можете запускати системні процесори (якщо прав вистачить):
{$_modx->runProcessor('resource/update', [
'id' => 10,
'alias' => 'test',
'context_key' => 'web',
])}

Перевірка авторизації

Так як об'єкта з користувачем в
{$_modx}
ні, методи перевірки авторизації прав доступу винесені безпосередньо в клас:
{$_modx->isAuthenticated()}
{$_modx->hasSessionContext('web')}
{$_modx->hasPermission('load')}

Інші методи

Ці методи повинні бути знайомі всім розробникам MODX, тому просто покажу їх на прикладах:
{$_modx->regClientCss('/assets/css/style.css')}
{$_modx->regClientScript('/assets/css/script.js')}

{$_modx->sendForward(10)}
{$_modx->sendRedirect('http://yandex.ru')}

{$_modx->setPlaceholder('key', 'value')}
{$_modx->getPlaceholder('key')}

{if $res = $_modx->findResource('url-to/doc/')}
{$_modx->sendRedirect ($_modx->makeUrl($res) )}
{/if}

Розширення шаблонів
Використання шаблонизатора Fenom дозволяє включати одні чанкі (шаблони інші) і навіть розширювати їх.

Наприклад, ви можете просто довантажити вміст чанка:
Звичайний чанк {include 'ім'я чанка'}
Шаблон modTemplate {include 'template:ім'я шаблону'}
Чанк з набором параметрів
{include 'chunk@propertySet'}
{include'template:Name@propertySet'}

Детальніше про {include} читайте в офіційній документації.

Набагато більш цікава функція — {extends} шаблонів, вона вимагає включеної системної налаштуванняpdotools_fenom_parser.

Пишемо базовий шаблон "Fenom Base":
<!DOCTYPE html>
<html lang="en">
<head>
{include 'head'}
</head>
<body>
{block 'navbar'}
{include 'navbar'}
{/block}
<div class="container">
<div class="row">
<div class="col-md-10">
{block 'content'}
{$_modx->resource.content}
{/block}
</div>
<div class="col-md-2">
{block 'sidebar'}
Sidebar
{/block}
</div>
</div>
{block 'footer'}
{include 'footer'}
{/block}
</div>
</body>
</html>

Він включає звичайні чанкі (в яких, до речі, звичайні плейсхолдеры MODX від компонентаTheme.Bootstrap) і визначає кілька блоків
{block}
, які можна розширити в іншому шаблоні.

Тепер пишемо "Fenom Extended":
{extends 'template:Fenom Base'}
{block 'content'}
<h3>{$_modx->resource.pagetitle}</h3>
<div class="jumbotron">
{parent}
</div>
{/block}

Так ви можете написати один базовий шаблон і розширити його дочірніми.

Точно також можна писати і розширювати чанкі, тільки зверніть увагу, що для роботи з modTemplate потрібно указувати префікс template:, а для чанків немає — вони працюють за умовчанням
{include}
та
{extends}
.

Тестування продуктивності
Створюємо новий сайт і додаємо в нього 1000 ресурсів ось таким консольним скриптом:
<?php
define('MODX_API_MODE', true);
require 'index.php';

$modx->getService('error','error.modError');
$modx->setLogLevel(modX::LOG_LEVEL_FATAL);
$modx->setLogTarget(XPDO_CLI_MODE ? 'ECHO' : 'HTML');

for ($i = 1; $i < = 1000; $i++) {
$modx->runProcessor('resource/create', array(
'parent' => 1,
'pagetitle' => 'page_' . rand(),
'template' => 1,
'published' => 1,
));
}

Потім створюємо 2 чанка:
modx
та
fenom
з наступним вмістом, відповідно:
<p>[[+id]] - [[+pagetitle]]</p>

і
<p>{$id} -{$pagetitle}</p>

І додаємо два консольних скрипта тестування. Для рідного парсера MODX
<?php
define('MODX_API_MODE', true);
require 'index.php';

$modx->getService('error','error.modError');
$modx->setLogLevel(modX::LOG_LEVEL_FATAL);
$modx->setLogTarget(XPDO_CLI_MODE ? 'ECHO' : 'HTML');

$res = array();
$c = $modx->newQuery('modResource');
$c->select($modx->getSelectColumns('modResource'));
$c->limit(10);
if ($c->prepare() &&$c->stmt->execute()) {
while ($row = $c->stmt->fetch(PDO::FETCH_ASSOC)) {
$res .= $modx->getChunk('modx', $row);
}
}
echo number_format(microtime(true) - $modx->startTime, 4),'s<br>';
echo number_format(memory_get_usage() / 1048576, 4),'mb<br>';
echo$res;

І для pdoTools:
<?php
define('MODX_API_MODE', true);
require 'index.php';

$modx->getService('error','error.modError');
$modx->setLogLevel(modX::LOG_LEVEL_FATAL);
$modx->setLogTarget(XPDO_CLI_MODE ? 'ECHO' : 'HTML');
$pdoTools = $modx->getService('pdoTools');

$res = array();
$c = $modx->newQuery('modResource');
$c->select($modx->getSelectColumns('modResource'));
$c->limit(10);
if ($c->prepare() &&$c->stmt->execute()) {
while ($row = $c->stmt->fetch(PDO::FETCH_ASSOC)) {
$res .= $pdoTools->getChunk('fenom', $row);
//$res .= $pdoTools->getChunk('modx', $row);
}
}
echo number_format(microtime(true) - $modx->startTime, 4),'s<br>';
echo number_format(memory_get_usage() / 1048576, 4),'mb<br>';
echo$res;

Так як pdoTools розуміє обидва синтаксису, для нього 2 тіста — в режимі тегів MODX, і в режимі Fenom.
В скриптах є вказівка limit = 10, далі в таблиці я наводжу цифри з його зростанням:






Limit MODX pdoTools (MODX) pdoTools (Fenom) 10 0.0369 s 8.1973 mb 0.0136 s 7.6760 mb 0.0343 s 8.6503 mb 100 0.0805 s 8.1996 mb 0.0501 s 7.6783 mb 0.0489 s 8.6525 mb 500 0.2498 s 8.2101 mb 0.0852 s 7.6888 mb 0.0573 s 8.6630 mb 1000 0.4961 s 8.2232 mb 0.1583 s 7.7019 mb 0.0953 s 8.6761 mb
А тепер, давайте трохи ускладнимо чанкі — додамо в них генерацію посилання для ресурсу та висновок
menutitle
:
<p><a href="[[~[[+id]]]]">[[+id]] - [[+menutitle:default=`[[+pagetitle]]`]]</a></p>

і
<p><a href="{$_modx->makeUrl($id)}">{$id} - {$menutitle ?: $pagetitle}</a></p>







Limit MODX pdoTools (MODX) pdoTools (Fenom) 10 0.0592 s 8.2010 mb 0.0165 s 7.8505 mb 0.0346 s 8.6539 mb 100 0.1936 s 8.2058 mb 0.0793 s 7.8553 mb 0.0483 s 8.6588 mb 500 0.3313 s 8.2281 mb 0.2465 s 7.8776 mb 0.0686 s 8.6811 mb 1000 0.6073 s 8.2560 mb 0.4733 s 7.9055 mb 0.1047 s 8.7090 mb
Як бачите, обробка чанків через pdoTools у всіх випадках швидше.
При цьому помітно, що у чанків Fenom є певний мінімум для старту, який обумовлений необхідністю компіляції шаблону.

Висновок
Давайте підсумуємо можливості парсера pdoTools:
  • Швидка робота
  • Завантаження чанків з різних джерел, включаючи файли
  • Підтримка тегів fastField
  • Підтримка шаблонизатора Fenom
    • Спадкування шаблонів
    • Розширення шаблонів

    • Безпечний доступ до просунутим функцій MODX
На даний момент pdoTools викачаний більше 40 000 разів офіційного репозиторію і понад 10 000 репозиторію modstore.pro, що дозволяє сподіватися на широке розповсюдження нових технологій шаблонізації в MODX.

Велике спасибі хабраюзеру aco за чудовий шаблонизатор!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.