ZoomX - альтернативная шаблонизация

Версия 2.1.0-beta

Введение

Главная идея данного компонента - полный отказ от стандартного шаблонизатора в пользу PHP шаблонизаторов. Никаких элементов в БД, многократного парсинга контента и логики в стиле сниппета IF. Теперь вся разработка в удобном IDE редакторе, плюс полноценная поддержка версионности, а также мощный функционал и огромное сообщество разработчиков. Для управления шаблонами добавлена библиотека FastRoute, которая позволяет гибко управлять маршрутизацией запросов. Но в отличие от других пакетов MODX с подобным функционалом (CustomRequest, modxFastRouter, VirtualPage), которые срабатывают на событие OnPageNotFound после того как MODX не нашёл указанного ресурса, в ZoomX FastRoute запускается перед стандартным маршрутизатором MODX и имеет 3 режима работы - отключён, совместный и строгий.

Кроме того на основе FastRoute реазизована поддержка REST архитектуры, что позволяет строить полноценные API приложения.

Внимание!

Данный компонент требует PHP ≥ 7.0.

Использование

Установите компонент используя стандартный установщик пакетов. Включите системную настройку friendly_urls. Следующим шагом нужно настроить роуты и Smarty шаблоны. Из коробки идут 3 шаблона - "index.tpl", "base.tpl" и "error.tpl". Изначально шаблоны находятся в папке core/components/zoomx/templates/default/. Но её можно переопределить. Даже нужно. Ведь эти шаблоны будут перезаписываться при каждом обновлении компонента.

За работу с шаблонами отвечают 2 системные настройки - zoomx_template_dir (по-умолчанию, core/components/zoomx/templates/) и zoomx_theme (по-умолчанию, default). Я советую перенести шаблоны в папку core/templates/default/.

Темы используются для удобного разделения представлений. Например, шаблоны для десктопной версии сайта можно разместить в теме "default", а мобильную версию, соответственно, в папке "mobile". Если темы совсем не нужны, то настройку zoomx_theme можно оставить пустой.

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

Маршрутизатор

Маршруты (роуты) настраиваются в файле core/config/routes.php. Чтобы подключить шаблон, нужно вернуть объект класса ZoomView. Альтернативный вариант - использовать хелпер viewx().
# 1. Класс
$router->get('hello.html', function() {
    // Нужно вернуть объект класса ZoomView. Первый параметр - название шаблона, второй - данные для шаблона.
    return new ZoomView('hello.tpl', ['name' => 'John']);
});
# 2. Хелпер
$router->get('users/{id}', function($id) use($modx) {
    $user = $modx->getObject('modUser', ['id' => (int)$id]);
    // Можно использовать хелпер.
    return viewx('profile.tpl', $user->toArray());
});

Подробнее про роуты можно узнать в документации FastRoute.

Важно!

Обратите внимание, что URI в роутах должен совпадать с URI ресурса. Именно поэтому в примерах выше URI указаны без начального слэша в отличие от документации FastRoute. Слэш нужно указывать только для главной страницы.

Можно вернуть просто строку:

$router->get('hello.html', function() {
    return '<h1>Hello, John!</h1>';
});

Пример редиректа. Можно использовать как стандартный метод modX::sendRedirect(), так и переопределить переменную $modx->resourceIdentifier, указав в ней или id нужного ресурса или URI. В последнем случае для переопределённого ресурса будет использован указанный шаблон.

$router->get('product1.html', function() use($modx) {
    $modx->sendRedirect('catalog/product2.html');
});
// Использование идентификатора ресурса
$router->get('resource.html', function() use($modx) {
    // можно указать id
    $modx->resourceIdentifier = 2;  
    // или URI ресурса
    $modx->resourceIdentifier = 'another.html';
    
    return viewx('hello.tpl', ['name' => 'John']);
});

Что важно понимать? Так как маршрутизатор срабатывают в самом начале запроса, то в роутах ресурс ещё не определён ($modx->resource = null). Менеджер запроса подгрузит его позже. Но если вам очень нужен ресурс именно в роуте, то придётся самостоятельно его запросить. В этом случае автоматический поиск задействован не будет.

$router->get('resource.html', function() use($modx) {
    $modx->request->getResource('', 'resource.html');
    return viewx('resource.tpl');
});

Режимы маршрутизации

Маршрутизатор может работать в 3-х режимах:

  • 0 – отключен. Все указанные роуты игнорируются.
  • 1 – смешанный (мягкий). Если для указанного URI роут не найден, то MODX продолжит обработку запроса в обычном режиме. Т.е. вы можете работать в режиме PHP шаблонизатора с отдельными ресурсами.
  • 2 – строгий (монопольный). Если для указанного URI роут не найден, то обработка запроса будет завершена с ошибкой 404. Шаблонизатор MODX не запускается.

Режимы указываются в системной настройке zoomx_routing_mode.

Если с первым и последним режимами всё понятно, то в работе смешанного режима есть свои тонкости. Например, может быть такая ситуация, когда роут для указанного URI найдён, а ресурса с таким URI нет. Это может случиться для роутов с масками.

$router->get('blog/{uri:.+}', ['ArticleController', 'show']);

Для запроса site.ru/blog/abra-cadabra-fake указанный выше роут подойдёт, но ресурса с таким URI найдено не будет. Если бы роут не был найден, то ZoomX передал бы дальнейшую обработку запроса MODX и последний сам бы занимался поиском ресурса. Но так как роут сработал, то ZoomX будет работать как в строгом режиме. Т.е. выведет свою страницу ошибки. Таким образом, ключевым моментом является наличие роута для URI запроса.

Контроллеры

Всю логику обработки роутов лучше переносить в контроллеры. Это стандартная практика в мире фреймворков. Например, ресурс site.ru/users используется для работы с пользователями:

// Список пользователей
$router->get('users', ['Zoomx\Controllers\UserController', 'index']);
// Данные конкретного пользователя
$router->get('users/{id:\d+}', ['Zoomx\Controllers\UserController', 'index']);
// Добавить пользователя
$router->post('users', ['Zoomx\Controllers\UserController', 'create']);

Метод index можно опустить - достаточно указать только класс контроллера

$router->get('users', Zoomx\Controllers\UserController::class);

К сожалению формы умеют работать только с методами GET и POST. Для того, чтобы использовать другие HTTP методы, необходимо включить системную настройку "zoomx_http_method_override" (по-умолчанию, включена) и добавить в форму скрытый инпут с именем "_method" и названием метода.

<form>
    <input type="hidden" name="_method" value="PUT">
    ...
</form>

После этого будет работать следующий роут

// Обновление пользователя
$router->put('users/{id:\d+}', ['Zoomx\Controllers\UserController', 'update']); // в методе update нужно указать параметр $id

Внимание!

Для некоторых роутов самостоятельно определять ресурс $modx->resource.

В большинстве случаев в обычном режиме с методами PUT, PATCH и DELETE работают редка. В основном это используется на API режиме.

Контроллеры располагаются в папке core/conponents/zoomx/src/Controllers/. Пользовательский контроллер можно разместить в ней. Этот контроллер должен расширять базовый контроллер ZoomX\Controllers\Controller.

<?php

namespace Zoomx\Controllers;

class UserController extends Controller
{
    public function index()
    {
        return viewx('users.tpl');
    }

    public function show($id)
    {
        // Самосоятельно находим ресурс, предназначенный для вывода данных пользователя.
        $this->modx->request->getResource('alias', 'users');
        // Находим пользователя.
        $user = $this->modx->getObject('modUser', ['id' => $id]);
        
        return viewx('user.tpl', compact('user'));
    }


}

RESTful API

Кроме управления шаблонами FastRoute предлагает полноценный механизм RESTful API. В MODX уже есть встроенный функционал поддержки этого архитектурного стиля. Но он крайне ограничен и отстаёт от современных стандартов. Он требует создание отдельной точки входа и может работать только с одной сущностью.

Принцип построения REST запросов

Метод HTTP URI Метод контроллера
GET /users index
GET /users/create create
POST /users store
GET /users/{id} show
GET /users/{id} edit
PUT/PATCH /users/{id} update
DELETE /users/{id} delete

Шаблонизатор Smarty

Smarty мощный и один из самых быстрых PHP шаблонизаторов. Так как он входит в ядро MODX, то выбор пал на него. У него много предустановленных плагинов (встроенные функции, пользовательские функции, встроенные модификаторы). ZoomX также добавляет несколько своих плагинов.

Но кроме обычного синтаксиса с модификаторами, Smarty в ZoomX умеет работать и с синтаксисом в стиле MODX - {'*pagetitle'}, {'%lexicon'}, {'++setting'}, {'~5'}.

Информация!

В шаблонах доступен объект $modx, доступом к которому можно управлять системной настройкой zoomx_include_modx.

Модификаторы

chunk – вызвать чанк.

Параметр:

  • (array) properties - ассоциативный массив параметров.
// Кэшированный чанк
{'chunkName'|chunk:['foo' => 'bar']}
// Некэшированный чанк (Smarty syntax)
{'chunkName'|chunk:['foo' => 'bar'] nocache}
config – получить системную настройку.

Параметр:

  • (mixed) default - значение по-умолчанию в случае отсутствие указанной системной настройки.
//[[++site_name]]
{'site_name'|config}
css,
csstohead – зарегистрировать CSS в секции HEAD страницы.

Параметр:

  • (string) media - тип медиа.
{'assets/css/styles.css'|csstohead}
{'assets/css/styles.css'|csstohead:all}
html,
htmltobottom – зарегистрировать html блок в конце страницы.
{'HTML content'|htmltobottom}
htmltohead – зарегистрировать HTML блок в секции HEAD страницы.
{'HTML content'|htmltohead}
js,
jstobottom – зарегистрировать JS в конце страницы.

Параметр:

  • (bool) plaintext - указывает, является ли указанное значение ссылкой на файл (false) или яваскрипт текстом (true).
{'assets/js/scripts.js'|jstobottom}
{'<script>let foo = "bar";</script>'|jstobottom:true}
jstohead – зарегистрировать JS секции HEAD страницы.

Параметр:

  • (bool) plaintext - указывает, является ли указанное значение ссылкой на файл (false) или яваскрипт текстом (true).
{'assets/js/scripts.js'|jstohead}
{'<script>let foo = "bar";</script>'|jstohead:true}
ignore – не парсить указанный тег, а вывести как есть.
{'content'|resource|ignore}  // вывод: {'content'|resource}
// тоже самое используя стандартный синтаксис Smarty
{literal}
{'content'|resource}
{/literal}
lexicon – вывести значение лексикона для указанного ключа.

Параметры:

  • (array) params - массив плейсхолдеров.
  • (string) language - код языка (если лексикон уже загружен) или строка параметрами в формате "язык:пространство_имён:топик", чтобы предварительно загрузить лексикон. Подробнее в официальной документации MODX.
# Вывести строку для уже загруженного лексикона.
//[[%lang]]
{'lang_key'|lexicon}
// with parameters
{'lang_key'|lexicon:['foo' => 'bar']}
# Загрузить лексикон перед обработкой
lang_key 
modx – распарсить тег MODX. Может быть использован для парсинга тега ресурса или TV, в контенте которых есть теги. Используется парсер, указанный в настройке parser_class.
{'[[*pagetitle]] - [[++site_name]]'|modx}
parse – распарсить тег. Как и предыдущий модификатор может быть использован для парсинга тега ресурса или TV, в контенте которых есть теги. Но в отличие от предыдущего можно указывать какой парсер использовать.

Параметр:

  • (string) parser - класс парсера. По-умолчанию, используется указанный в системной настройке zoomx_parser_class.
// Использовать ZoomX парсер, указанный в настройке "zoomx_parser_class".
{'content'|resource|parse}
// Использовать парсер MODX.
{'[[*pagetitle]] - [[++site_name]]'|parse:'modParser'}
ph – получить MODX плейсхолдер.
//[[+modx.user.id]]
{'modx.user.id'|ph}
print – вывести отформатированное и экранированное значение.

Параметры:

  • (bool) pre - форматировать вывод. Оборачивает вывод в тег <pre>. По-умолчание, true.
  • (bool) esc - экранировать вывод. Использует функцию htmlspecialchars(). По-умолчание, true.
{$array|print}
// вывести необработанную строку
{$array|print:false:false}
resource – получить значение указанного поля текущего ресурса.

Параметр:

  • (string) defaultField - альтернативное поле ресурса, которое будет выведено, если начальное поле пустое.
{'pagetitle'|resource}
// значением по-умолчанию является другое поле ресурса.
{'longtitle'|resource:'pagetitle'}
// значение по-умолчанию является строкой.
{'longtitle'|resource|default:'Строка'}
// TV значение
{'tv'|resource}
snippet – запустить MODX сниппет.

Параметр:

  • (array) properties - массив параметров.
// Кэшированный сниппет
{'snippetName'|snippet:['foo' => 'bar']}
// Некэшированный сниппет (Smarty syntax)
{'snippetName'|snippet:['foo' => 'bar'] nocache}
// Некэшированный сниппет в набором параметров
{'snippetName@PropSet'|snippet:['foo' => 'bar'] nocache}
tv – вывести TV текущего ресурса. Альтернатива модификатору resource для вывода TV.
{'tv_name'|tv}
url - генерировать URL для указанного ресурса. Подробности в официальной документации MODX.

Параметры:

  • (string|array) context - контекст. С версии 2.0 можно указать массив со всеми перечисленными параметрами.
  • (array) args - массив агрументов URL.
  • (integer|string) scheme - схема.
  • (array) options - массив опций.
{5|url}
{5|url:'web':['foo' => 'bar']:-1}
# С версии 2.0
{5|url:['scheme' => 'full']}
user – получить значение поля текущего пользователя.

Параметр:

  • (mixed) default - значение по-умолчанию, если указанное поле пользователя пустое (null).
{'username'|user}
{'fullname'|user:'username'}

Блоки

auth – выводит содержимое блока только для залогиненных пользователей.
{auth}
Контент для аутентифицированных пользователей.
{/auth}
guest – выводит содержимое блока только для гостей.
{guest}
Контент для неаутентифицированных пользователей.
{/guest}
modx – используется для парсинга содержимого блока с помощью MODX парсера, указанного в системной настройке parser_class.
{modx}
<a href="[[~[[*id]]]]">[[*pagetitle]]</a>
{/modx}
parse – используется для парсинга содержимого блока. По-умолчанию используется парсер, указанный в системной настройке zoomx_parser_class. А там по-умолчанию указан ZoomSmarty. Хотя можно указать и любой MODX парсер. Но для этого лучше использовать соответствующий блок modx.

Параметр:

  • (string) parser - класс парсера.
{parse}
<a href="[[~[[*id]]]]">[[*pagetitle]]</a>
{/parse}

// Можно указать любой другой
{parse parser="pdoParser"}
<a href="[[~[[*id]]]]">[[*pagetitle]]</a>
{/parse}

Создание собственных плагинов

Для большей части задач можно использовать непосредственно функции PHP. Например, вместо {$string|trim} можно вызвать {trim($string)}. Если данных возможностей не хватает, то можно создать собственный модификатор или функцию. Сделать это можно двумя способами:

  1. Создать файл плагина следуя инструкции и положить его в папку плагинов Smarty (системную или пользовательскую). Он будет подключён автоматически. Если вы создали свою собственную папку плагинов, то не забудьте указать её в системной настройке smarty_custom_plugin_dir.
  2. Динамически подключить во время загрузки MODX с помощью метода registerPlugin(). Для этого необходимо создать плагин на событие OnHandleRequest и в нём подключить нужный функционал:
    <?php
    // OnHandleRequest
    
    function print_current_date($params, $smarty)
    {
      if(empty($params["format"])) {
        $format = "%b %e, %Y";
      } else {
        $format = $params["format"];
      }
      return strftime($format,time());
    }
    
    // Подключение функции
    parserx()->registerPlugin("function","date_now", "print_current_date");
    // Пробросить PHP функцию ucfirst в виде модификатора.
    $smarty->registerPlugin("modifier","ucfirst", "ucfirst");
    

    В шаблоне теперь доступна функция date_now и модификатор ucfirst.

    {date_now}
    {* Или с форматированием *}
    {date_now format="%Y/%m/%d"}
    
    {$name|ucfirst}
    

Кэширование

В Smarty заложены мощные возможности кэширования. Вы можете управлять кэшированием отдельных тегов также, как это делается в MODX с помощью знака !, но несколько иначе - используя специальный аттрибут nocache или одноимённый блок.

// Аттрибут
{'username'|user nocache}
// Блок
{nocache}
{'username'|user}
{'email'|user}
{/nocache}

Для подшаблонов (чанков по-нашему) возможностей больше. Для них можно указать даже время кэширования.

{include file="chunks/header.tpl" cache_lifetime=300}

Можно указать разные кэши одного подшаблона для разных условий

// Разные кэши для разных категорий
{include file="header.tpl" cache_id="cache_key_{$modx->resource->parent}" title="{$modx->resource->pagetitle}"}

Управлять кэшированием можно с помощью системной настройки zoomx_caching. При разработке советую её отключать. Также кэш не будет создаваться для документов, у которых снят чекбокс "кэшируемый".

Информация!

Компилированные файлы шаблонов Smarty хранятся в папке, указанной в системной настройке zoomx_smarty_compile_dir. Очистить их можно или вручную или через механизм очистки кэша в админке. Файлы кэша хранятся в папке, указанной в системной настройке zoomx_smarty_cache_dir. Эта папка очищается при сохранении в админке любого элемента, ресурса, системной настройки и т.д. Данный подход является стандартным в MODX.

Управление ошибками

В MODX для ошибок 401 (Unauthorized), 403 (Forbidden), 404 (Page Not Found) и 503 (Service Unavailable) предусмотрены отдельные страницы. Если вдруг в процессе возникнут другие ошибки, то у MODX на этот счёт предусмотрены статические файлы, которые находятся в папке /core/error/.

В ZoomX список ошибок значительно больше. Вот их полный список:

  • 400 – Bad Request. Вызывается пользователем.
  • 401 – Unauthorized. Системный.
  • 403 – Forbidden. Системный.
  • 404 – Not Found. Системный.
  • 405 – Method Not Allowed. Системный.
  • 406 – Not Acceptable. Вызывается пользователем.
  • 415 – Unsupported Media Type. Вызывается пользователем.
  • 503 – Service Unavailable. Системный.

Этот список можно расширять самостоятельно с помощью исключений.

Как мы уже выяснили выше, в режимах "Отключён" и "Смешанный" (в случае, когда роут для текущего ресурса не определён), ошибками занимается MODX. Он или переадресует на указанную в соответствующей системной настройке ("error_page", "unauthorized_page" или "site_unavailable_page") страницу ошибки, или выводит статическую страницу ошибки из директории core/error/.

В строгом режиме, а также в смешанном (когда роут для указанного URI определён), но ресурса с таким URI нет, или сгенерирована ошибка (об этом дальше), ZoomX выведет собственную страницу ошибки на основе шаблона error.tpl.

Шаблоны ошибок

Для вывода ошибок в компонент добавлен универсальный шаблон error.tpl. Если необходим уникальный шаблон для какой-то ошибки, то нужно создать файл, в названии которого указан код ошибки. Т.е. для ошибки 401 файл шаблона должен называться 401.tpl. Это шаблон подхватится автоматически для данной ошибки.

Исключения

В ZoomX все ошибки работают на основе исключений. Из коробки идут 9 Http исключений – одно базовое, остальные его потомки. Найти их можно в папке компонента core/components/zoomx/src/Exceptions/. Это системные исключения. Их изменять нельзя. Но вы можете легко создавать свои по аналогии и даже подменять системные. Для этого есть специальный файл конфига, который находится в core/config/.

≶?php

return [
    // Подменяет системное исключения для ошибки 404.
    404 => Zoomx\Exceptions\MyNotFoundHttpException::class,
    // Добавляет исключение для кастомной ошибки "4041 Пользователь не найден".
    4041 => Zoomx\Exceptions\UserNotFoundHttpException::class, 
];

После этого вы можете использовать эти исключения в роутах.

$router->get('users/{id:\d+}', function(int $id) use ($modx) {
    if (!$user = $modx->getObject('modUser', ['id' => $id])) {
        abortx(4041); // Выведется страница ошибки error.tpl, если нет шаблона 4041.tpl   
    }
    return viewx('profile.tpl', $user);
});

Из исключения в шаблон передаются следующие данные:

  • $code – код ошибки. В HTTP исключениях код идентичен HTTP статус коду. В пользовательских исключениях может переопределяться.
  • $status – HTTP статус код ответа.
  • $title – заголовок.
  • $detail – текст сообщения об ошибке.

Для того, чтобы изменить текст или заголовок сообщения не обязательно создавать своё исключение. Достаточно передать эти данные в параметрах хелпера abortx() для системного исключения.

$router->get('users/{id:\d+}', function(int $id) use ($modx) {
    if (!$user = $modx->getObject('modUser', ['id' => $id])) {
        abortx(404, 'Текст сообщения об ошибке', 'Заголовок'); // Используется системное исключение 404.   
    }
    return viewx('profile.tpl', $user);
});

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

// routes.php

if (какое-то условие && пользователь не админ) {
   abortx(503);  // Отключаем сайт
}

// Определения роутов
...

Событие OnRequestError

В MODX есть всего 2 события для ошибок (OnPageNotFound и OnPageUnauthorized). Их явно не хватает для других ошибок. Поэтому ZoomX добавляет общее событие для всех ошибок "OnRequestError".

Системные настройки

Ключ По-умолчанию Описание
zoomx_caching true Кэшировать файлы шаблонов.
zoomx_theme default Имя папки в каталоге шаблонов. Позволяет управлять темами сайта. Добавляется в путь, указанный в настройке zoomx_template_dir. Если темы не нужны, можно указать пустое значение.
zoomx_template_dir {core_path}components/zoomx/templates/ Полный путь к файлам шаблонов. Рекомендую перенести в {core_path}templates/
zoomx_routing_mode 1 Режим роутинга. Возможные значения:
0 - роутинг выключен;
1 - смешанный режим (если роут для данного URI не найден, поиском займётся MODX);
2 - строгий режим (если роут не найден, то обработка завершится с ошибкой 404).
zoomx_include_modx true Разрешить объект $modx в шаблонах.
modx_parser_class ZoomSmarty Класс парсера, который используется для обработки контента. Должен имплементировать интерфейс Zoomx\ParserInterface.
zoomx_http_method_override true Позволяет указать HTTP методы "PATCH", "PUT" и "DELETE" в элементе формы с именем "_method".
zoomx_autoload_resource true Автоматический поиск ресурса для указанного маршрута. При отключении позволяет использовать виртуальные страницы.
zoomx_smarty_cache_dir zoomx/smarty/cache/ Путь к файлам кэша шаблонов Smarty относительно папки core/cache/.
zoomx_smarty_compile_dir zoomx/smarty/compile/ Путь к компилированным файлам шаблонов Smarty относительно папки core/cache/.
zoomx_smarty_config_dir {core_path}config/ Полный путь к файлам конфигов.
zoomx_smarty_custom_plugin_dir '' Полный путь к пользовательским плагинам Smarty.
zoomx_default_tpl error.tpl Используется в строгом режиме для вывода сообщений об ошибках. Этот же шаблон будет использован для вывода ошибок в смешанном режиме, если в роуте будет вызвано исключение.
zoomx_template_extension tpl Расширение файлов шаблонов Smarty.
zoomx_modx_tag_syntax true Позволяет использовать синтаксис в стиле MODX тегов - {'*pagetitle'}, {'++setting'}, {'~5'} и {'%lexicon'}. Негативно сказывается на производительности.
  • Видео с небольшой демонстрацией идеи.
  • Проект на GitHub.
Выделите опечатку и нажмите Ctrl + Enter, чтобы отправить сообщение об ошибке.