Skip to content

pbFenom ​

Шаблоны рендерит pbFenom — наш форк Fenom, подключённый как пакет Composer pageblocks/fenom. Это обязательная зависимость, а не опция: View типизирован по \pbFenom, а движок поднимается через factory() у его тонкого наследника Boshnik\PageBlocks\View\Fenom — он существует ровно затем, чтобы объявить свойство $data, из которого читает аксессор {$.pb}.

json
"require": {
    "pageblocks/fenom": "^1.0"
}

Сам язык шаблонов не менялся. Всё, что вы знаете про синтаксис Fenom — {if}, {foreach}, модификаторы, {extends}, {include} — работает так же. Поменялось то, что под ним.

Что где ​

СтраницаО чём
Быстрый стартОтрисовать первый шаблон
Типы шаблоновЧанк, файл, именованный источник, строка — и где они лежат
НастройкиКэш, автоперезагрузка, каталоги
Доступные данныеЧто уже есть в шаблоне и глобальные переменные
МодификаторыГотовые и свои
Псевдо-тегиВызов функции как тега
Блочные тегиСвои парные конструкции
Функции 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 модификаторам добавили типы параметров, и обычная пустая переменная шаблона стала фаталом:

php
{$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, чтобы артефакты от старого генератора кода не переиспользовались.

Строгие типы ​

Во всех 18 файлах исходников объявлен strict_types=1, и это вскрыло четыре скрытых бага, которые слабый режим тихо замазывал. Среди них — getProvider(), получавший булево значение при каждой загрузке шаблона: strstr() возвращает false, а не null, когда в имени шаблона нет схемы.

Скомпилированные артефакты строгие типы намеренно не объявляют: данные шаблона произвольны, и приведение целого, попавшего в модификатор со строковым типом, — это документированное поведение.

Скорость ​

Форк догонял не upstream, а собственные узкие места — поэтому цифры ниже это замеры «до и после» на одном и том же стенде, а не сравнение с другими движками.

ЧтоБылоСтало
Компиляция мегабайта {foreach}35,8 с0,34 с
Страница с {extends} и двадцатью {include}, auto_reload включён25,3 мкс7,6 мкс
То же без auto_reload7,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 и провайдеры шаблонов — см. Настройки.

© PageBlocks 2019-present