pbFenom
Шаблоны рендерит pbFenom — наш форк Fenom, подключённый как пакет Composer pageblocks/fenom. Это обязательная зависимость, а не опция: View типизирован по \pbFenom и поднимает движок через \pbFenom::factory().
"require": {
"pageblocks/fenom": "^1.0"
}Сам язык шаблонов не менялся. Всё, что вы знаете про синтаксис Fenom — {if}, {foreach}, модификаторы, {extends}, {include} — работает так же. Поменялось то, что под ним.
Что где
| Страница | О чём |
|---|---|
| Быстрый старт | Отрисовать первый шаблон |
| Типы шаблонов | Чанк, файл, шаблон MODX — и где они лежат |
| Настройки | Кэш, автоперезагрузка, каталоги |
| Доступные данные | Что уже есть в шаблоне и глобальные переменные |
| Модификаторы | Готовые и свои |
| Псевдо-теги | Вызов функции как тега |
| Блочные теги | Свои парные конструкции |
| Функции PHP | Что разрешено вызывать из шаблона |
| Обработка результата | Что происходит с готовым HTML |
| Запросы в шаблоне | query() прямо в разметке |
Где что объявлять
Свои модификаторы, теги и данные живут в слое сайта, а не в компоненте — обновление компонента их не тронет:
core/App/Helpers/fenom/
├── modifiers.php ← модификаторы
├── inline_tags.php ← псевдо-теги
├── block_tags.php ← блочные теги
├── data.php ← глобальные данные
└── php_functions.php ← разрешённые функции PHPЗаменить оригиналом нельзя
Форк живёт в своём пространстве имён: класс называется pbFenom, интерфейсы — pbFenom\ProviderInterface и прочие. Подставить fenom/fenom не выйдет, и поставить оба в надежде, что это один движок, — тоже.
Изоляция здесь и есть смысл: компонент фиксирует поведение, на которое опирается, вместо того чтобы следовать за меняющимся upstream.
Зачем форк
eval() больше нет
Upstream в нескольких режимах рендерил через eval() — а FORCE_COMPILE вообще писал скомпилированный файл на диск и потом его игнорировал. Форк подключает тот артефакт, который только что записал, поэтому opcache может держать опкоды; два режима, у которых файла нет по замыслу, рендерятся через приватную обёртку потока.
Практический эффект: ошибка времени выполнения называет шаблон, а не сообщает eval()'d code on line N.
Модификаторы снова принимают null
В upstream 3.0.0 модификаторам добавили типы параметров, и обычная пустая переменная шаблона стала фаталом:
{$x} {* нормально - htmlspecialchars(null) только предупреждает *}
{$x|escape} {* upstream 3.0.0: фатал *}В CMS пустое поле — норма, а не исключение. escape, unescape, truncate, strip, replace, ereplace, match, ematch и date считают null пустой строкой, как это было до 3.0.0.
Рекурсия ограничена
Циклический {include} раньше уходил в рекурсию, пока PHP не исчерпает стек вызовов, а циклический {insert} — пока не кончится память. И то и другое фатал, то есть непойманное: посетитель получал пустой 500, а в логе не было ничего полезного.
Теперь оба поднимают обычное исключение за миллисекунды и называют шаблон. Предел — pbFenom::$max_template_depth (32). Цена на горячем пути: 1,8% на цикле из тысячи {include}.
Вложенные ошибки рендера больше не заворачиваются повторно на каждом уровне — сообщения раньше читались как «unhandled exception in a: unhandled exception in b: ...».
addFunction() сохраняет свой контракт
Колбэк получает ($params, $tpl, $var), как и всегда. Если хочется, чтобы сигнатура самого колбэка стала API шаблона, регистрируйте через addFunctionSmart() — который в форке наконец принимает замыкания, массивы-колбэки и объекты с __invoke, а не только строки.
Пропущенный обязательный аргумент smart-функции теперь сообщается на этапе компиляции («Function excerpt requires the 'text' argument»), а не взрывается посреди рендера.
Подпись кэша учитывает конфигурацию
Регистрация функций не сбрасывала подпись кэша компиляции, поэтому два экземпляра движка, отличающиеся только зарегистрированной функцией, могли подсовывать друг другу чужие артефакты. Теперь подпись это покрывает, плюс появилась константа CACHE_FORMAT, чтобы артефакты от старого генератора кода не переиспользовались.
Строгие типы
Во всех 17 файлах исходников объявлен strict_types=1, и это вскрыло четыре скрытых бага, которые слабый режим тихо замазывал. Среди них — getProvider(), получавший булево значение при каждой загрузке шаблона: strstr() возвращает false, а не null, когда в имени шаблона нет схемы.
Скомпилированные артефакты строгие типы намеренно не объявляют: данные шаблона произвольны, и приведение целого, попавшего в модификатор со строковым типом, — это документированное поведение.
Скорость
Форк догонял не upstream, а собственные узкие места — поэтому цифры ниже это замеры «до и после» на одном и том же стенде, а не сравнение с другими движками.
| Что | Было | Стало |
|---|---|---|
Компиляция мегабайта {foreach} | 35,8 с | 0,34 с |
Страница с {extends} и двадцатью {include}, auto_reload включён | 25,3 мкс | 7,6 мкс |
То же без auto_reload | 7,2 мкс | 4,5 мкс |
Тысяча {include} со статическим именем | 385 мкс | 235 мкс |
| Таблица на тысячу строк | 454 мкс | 422 мкс |
Первая строка — не оптимизация, а починенная сложность: закрытие блочного тега копировало всё накопленное тело целиком, и время росло квадратом. Сгенерированный код при этом побайтово тот же.
Остальное — обычная работа с горячим путём: {include} со статическим именем разрешается один раз за рендер, а не на каждой итерации внешнего цикла; realpath() ушёл из проверки актуальности кэша; {foreach} привязывает ключ и значение к локальным ссылкам, и {$row.n} стоит одного поиска по хэшу вместо двух.
Отдельно про вес артефактов: покомментарийная отладка каждого тега занимала около 45% сгенерированного файла. Теперь она включается опцией debug_comments, а по умолчанию не тратит ни диск, ни память opcache.
Усиленное экранирование оказалось быстрее слабого
Флаги ENT_QUOTES|ENT_SUBSTITUTE|ENT_HTML5 вместо ENT_COMPAT — 202 нс против 279. Безопасность здесь ничего не стоила, а окупилась.
Безопасность
Ниже — не гипотетические риски, а то, что в upstream было объявлено и не работало.
{$.php} и {$.call} не проверялись вообще. Они доходили до call_user_func_array() без единой проверки: опция запрета вызовов PHP существовала, а код её не спрашивал. Теперь и запрет, и белый список нативных функций соблюдаются.
Запрет аксессоров был объявлен и никогда не проверялся — опция принималась, раскладывалась в настройки и не влияла ни на что.
Два фильтра вызовов блокировали всё. Условия складывались через И вместо ИЛИ, поэтому добавление второго правила запрещало любой колбэк — включая разрешённые первым.
{strip} необратимо выключал автоэкранирование. Тег писал не ту опцию, которую ему передали, и после первого же {strip} страница рендерилась без экранирования до конца запроса.
Экранирование пропускало апостроф и глотало данные. ENT_COMPAT оставлял ' сырым, а на битом UTF-8 возвращал пустую строку — то есть значение молча исчезало со страницы.
escape:'js' не закрывал выход из тега. Теперь стратегия шестнадцатеричная, и </script> внутри строки больше не закрывает элемент. Неизвестная стратегия бросает исключение, а не притворяется рабочей.
Плюс работа с файлами: каталог компиляции больше не сваливается в /tmp по умолчанию, общедоступный на запись каталог отвергается, артефакты пишутся с правами 0640. Имена кэша считаются sha256 — crc32 давал коллизии, то есть один шаблон мог получить скомпилированный код другого. Провайдер проверяет принадлежность пути с учётом разделителя, отвергает NUL и не удаляет сквозь символические ссылки.
Что вернулось и что появилось
{for} снова работает. Переход на PHP 8 в upstream удалил методы компилятора этого тега, а регистрацию оставил: документированный тег падал фаталом начиная с 3.0.0.
{use} и {paste} выдавали синтаксически неверный PHP — и кэшировали его, поэтому ошибка переживала правку шаблона.
addFunctionSmart() принимает любой callable. Раньше работали только строки: парсер звал strpos() по колбэку, и замыкание, массив-колбэк или объект с __invoke давали TypeError.
Из настроек новая одна — debug_comments, включающая покомментарийную отладку по требованию. disable_php_calls и disable_accessor существовали и до форка, но не проверялись; теперь проверяются.
Удалено
auto_trim / pbFenom::AUTO_TRIM — зарезервирован и бездействовал с 2013 года. Теперь его передача бросает Undefined option 'auto_trim', а не принимается молча и игнорируется. {autotrim} и опции тегов :trim / :ltrim / :rtrim были задокументированы десять лет и никогда не реализованы; их страницу убрали.
Версия
pbFenom::VERSION — 3.1, по поколению upstream, от которого форк отошёл. Версия пакета считается отдельно (pageblocks/fenom 1.x).
Где настраивается
Движок поднимается в View::init(): каталог кэша, auto_reload из настройки pageblocks_fenom_auto_reload и провайдеры шаблонов — см. Настройки.