Попал мне как-то под руку проект на WordPress (WP), где понадобилось сделать кастомную тему. В WP шаблоны нативные, что хорошо, - не надо учить дополнительный язык. Но очень захотелось понаследовать шаблоны как в Twig, а PHP из коробки так не умеет.
Осталось решить вопрос наследования. После изучения проблемы, вдохновляться было решено библиотекой phpti, в которой нашлось пару моментов, которые очень захотелось поправить:
-
Автор библиотеки большими буквами написал Every Block is Always Executed!, т. е. все блоки выполняются, даже если переопределены, и никогда не будут выведены.
-
Второй момент - библиотека под капотом использует анализ стека вызовов, что, как мне кажется, чуть-чуть читерство.
-
Третий момент - хочу ещё быстрее. В библиотеке активно используется
ob_start
на чём и попробуем сэкономить пару спичек.
Библиотека phpti построена вокруг основной конструкции
startblock/endblock
и наследования при помощи
import
в начале файла:
// layout.php<!-- разметка --><?php startblock('blockName') ?><?php endblock() ?><!-- разметка -->
// index.php<?php include 'layout.php' ?> <!-- указываем родительский шаблон --><?php startblock('blockName') ?> <!-- контент блока --><?php endblock() ?>
Некоторые наблюдения:
-
Вызовы вроде start/end можно заменить на анонимную функцию. Это избавит от необходимости сопоставлять вложенные старты и энды, а также сделает код контента ленивым.
-
Родительский шаблон указывается сверху файла. Это значит, что придётся сначала отрабатывать родительский, а потом дочерний шаблон. А значит придётся много чего буферизировать. А почему бы родителя не указать в конце?
-
Можно заметить, что на самом деле есть два разных типа блоков: одни определяются внутри шаблонов куда и выводят результат по месту, а другие по месту определения ничего не выводят, а только переопределяют родительские блоки.
С учётом вышесказанного, переделаем конструкцию на следующую:
// layout.php<!-- разметка --><?php slot('blockName', function(){ ?><?php }) ?><!-- разметка -->
// index.php<?php block('blockName', function(){ ?> <!-- контент блока --><?php }) ?><?php include 'layout.php' ?> <!-- указываем родительский шаблон -->
Разделив блоки на slot
и block
,
библиотеке больше не нужно пытаться понять, когда нужно выводить
результат, а когда нужно только переопределить блок.
Вроде выглядит не слишком монструозно по сравнению с оригинальной структурой. Посмотрим, покроет ли такой подход все хотелки.
root.php
- базовый шаблон, внизу иерархии:
<!DOCTYPE html><html> <head> <title><?php slot('title') /* слот - место в шаблоне */ ?></title> </head> <body> <div id="root"> <?php slot('body', function () { /* слот с контентом по дефолту */?> <p>'body' :: root.php</p> <?php }) ?> </div> </body></html>
two-columns.php
- промежуточный шаблон:
<?php block('title', function () { /* блок - контент для вставки в слот */?> Title :: two-columns.php<?php });block('body', function () { ?> <div id="two-columnts"> <div id="main"> <?php slot('main', function () { /* слот внутри блока */?> <p>'main' :: two-columns.php</p> <?php }) ?></div><div id="side"> <?php slot('side', function () { ?><p>'side' :: two-columns.php</p> <?php }) ?></div> </div> <div id="footer"> <?php slot('footer', function () { ?> <p>'footer' :: two-columns.php</p><?php }) ?> </div><?php });include './root.php'; // наследуем от root.php
index.php
- страница сайта, верхний шаблон:
<?phprequire_once '../src/InheritTpl.php'; block('title', function () { ?> 'title' :: index.php <?php });block('side', function () { ?> <p>'side' :: index.php</p><?php }); block('main', function () { ?> <div id="main-index"> <!-- Обернём содержимое от родителя --> <?php super() /* тут выводим контент из родительского блока */?> </div><?php });block('main', function () { /* Ещё раз тот же блок, почему бы и нет? */?> <div id="main-index"> <!-- И ещё раз обернём содержимое --> <?php super() ?> </div><?php });// А 'footer' пусть остаётся как былinclude './two-columns.php';
Результат рендеринга (отформатирован для читабельности):
<!DOCTYPE html><html> <head> <title> 'title' :: index.php </title> </head> <body> <div id="root"> <div id="two-columnts"> <div id="main"> <div id="main-index"> <!-- Обернём содержимое от родителя --> <div id="main-index"> <!-- И ещё раз обернём содержимое --> <p>'main' :: two-columns.php</p> </div> </div> </div> <div id="side"> <p>'side' :: index.php</p> </div> </div> <div id="footer"> <p>'footer' :: two-columns.php</p> </div> </div> </body></html>
Хотелки наследования удовлетворены. Но вот интересно, удалось ли сделать эту штуку быстрее?
Перепишем пример выше под библиотеку phpti. Дадим ей небольшую фору, т.к. в примере нет тяжеловесных переопределяемых блоков.
Сравнивать будем время 10,000 рендеров на PHP 8.0.2 и процессоре 3.6ГГц.
-
phpti: 0.831 секунд
-
сабж: 0.353 секунд
В качестве вывода можно сказать что размер библиотеки удалось сократить в 10 раз, при этом скорость работы механизма наследования увеличилась как минимум в 2 раза.
Посмотреть исходный код можно тут.