Шаблонизация в PHP
02-04-2024Время чтения ~ 14 мин.Albireo Framework / CMS 826
Шаблонизация — глобальная проблема в PHP. Под этим словом часто подразумевают разные методы и подходы, но в целом речь всегда идёт о том, чтобы упростить использование PHP в HTML. Ещё нужно иметь ввиду, что шаблонизация может означать два варианта. Первый — это шаблон сайта. Это как правило прерогатива CMS и может называться «темизацией», от слова «theme» — тема. Второй — это «template engine» — вывод готовых данных в виде суррогата, имитирующего php-код. По сути это всё одно и тоже. Шаблонизация — это интеграция логики и данных из PHP в HTML-структуру.
Для начала поговорим о «template engine», поскольку практически каждый php-разработчик с ними сталкивается. В качестве «движков» используются Django, Smarty, Twig, Blade — это наиболее популярные. Суть их работы в том, чтобы отдельно подготовить данные для вывода, а сам вывод с html-разметкой вынести в отдельный tpl-файл (см. MVC (Model-View-Controller) в PHP).
Не важно как именно организуется связь между этими файлами, главное это то, что происходит разделение логики приложения от непосредственного вывода.
«Движки» подобных tpl-шаблонизаторов имеют свой синтаксис, который рендерится в нативный код. В некоторых системах такой код кэшируется и сохраняется в отдельных файлах. Это делается для того, чтобы исключить повторный этап рендеринга одного и того же файлы вывода.
По сути шаблонизаторы создают замену нативному php-коду и здесь есть очень тонкая грань, которая делит удобство написания кода и ограничительные возможности самого «движка».
Например Twig — шаблонизатор достаточно популярный, но имеет огромное количество ограничений. Самое важное — это то, что в нём нельзя использовать нативный php-код. Это выглядит странно, но такова принципиальная позиция разработчика. Из-за того, что задачи по php всё-таки требуются, Twig оброс большим количеством суррогатного кода, имитирующего обычный PHP. Очевидно, что это борьба с ветряными мельницами будет продолжаться бесконечно — возможности PHP растут, задачи усложняются и Twig будет пытаться его имитировать и дальше.
Другая система шаблонизации Blade намного интересней, хотя тоже не лишена ограничений. Например он также не поддерживает PHP-код в обычном виде, но предлагает его заключать в специальные вставки @php ... @endphp
. У Blade есть одна особенность, которая его выделяет из всех других шаблонизаторов — это синтаксис.
Как правило все собственные конструкции шаблонизатора обрамляются в фигурные скобки {}
. Это историческая особенность, идущая от самого PHP — интерпретация ""
-строк и heredoc-синтаксис. В таких строках можно использовать вывод переменных и часто их обрамляют именно в фигурные строки.
Blade же отказался от этого варианта в пользу «собачек» @
. Проблема такого подхода в том, что а) он не настолько информативен, и б) он заставляет каждую такую конструкцию размещать на отдельных строках, а реальные задачи часто этого не допускают.
Впрочем, речь не об особенностях шаблонизаторов, а о том, что все они предназначены для замены нативного php-кода. Сам по себе PHP уже неплохой шаблонизатор и если просто придумать замены для <?php
, <?=
и ?>
мы уже получим более читабельный код. Именно такой вариант реализован в MaxSite CMS: PHP-шаблонизатор.
Основная сложность синтаксиса чистого PHP именно в угловых скобках — они совпадают с HTML-тэгами, что делает их смесь плохо читаемой.
Лично я вижу задачу шаблонизатора именно как помощника в написании php-кода, а не в том, чтобы загнать программиста за свои рамки. В Albireo CMS шаблонизатор позволяет выполнять произвольный php-код, включая нативный, как обычно, но при этом предлагает много полезных конструкций. Фактически он перекрывает Blade и добавляет другие возможности. Например есть @yield
, @math
, @loop
, расширенные @style
и @class
и т.п.
То есть задача не отказаться от PHP как шаблонизатора, а просто расширить его на дополнительный синтаксис. Если шаблонизатор начинает диктовать свои правила, то это плохой подход.
Итак, tpl-шаблонизатор отлично подходит для html-вывода отдельных блоков. Например вывод записей, пагинация, разметка виджетов — всё то, что можно отделить от логики, выносится отдельно в tpl-файлы и прогоняется через шаблонизатор. На выходе готовый html, который и выдаётся в браузер (или файл, всё равно).
Теперь обратимся к шаблонизации сайта в виде theme.
Это очень-очень-очень сложная задача... Я на ней уже семь собак съел, поэтому знаю о чём речь.
Примитивное понимание шаблона сайта состоит в том, что предполагается простенькое деление html-структуры страницы на отдельные файлы, вроде head.php
, content.php
, footer.php
и т.п. В каждом из этих файлов размещается свой кусок html-кода, который объединяется неким main.php
.
Такой подход, как это ни парадоксально, настойчиво предлагается разработчиками php-фреймворков. Это полнейшая туфта, которая является следствием того, что эти самые разработчики не делают реальных сайтов и не понимают практических задач сайтостроения.
Шаблон сайта — это та часть системы, которая может меняться от клиента к клиенту. Вариативность шаблона очень высока и объясняется тем, что в каждом из них используется свой стек библиотек. В одном шаблоне может быть Berry CSS, в другом Bootstrap, третий, прости господи, Tailwind. В одном jQuery, в другом Alpine. В одном иконки fontawesome, в другом тот же boootstrap. А количество галерей, слайдеров и им подобным зашкаливает.
Поэтому, когда разработчик php-фреймворка предлагает всё делать под принципу tpl-шаблонизации, то он элементарно не понимает, что файл шаблонизации будет разным в разных шаблонах сайта.
На сегодняшний день нет ни одного php-фреймворка, который предлагает хоть какой-то механизм решения этой проблемы. Суть её в том, что шаблон сайта — это не тоже самое, что и шаблонизация PHP в HTML.
Структура сайта всегда строится на разделении контента от его вывода. 20 лет назад всё делалось в каждом файле, но сейчас контент отделён от своего итогового вывода. Обычно в CMS на базах — это отдельное поле таблицы, где и хранится текст страницы. И система сама решает как и где его вывести, но всегда, подчеркиваю, всегда, выводит на уровне шаблона сайта.
Контент никогда не прогоняется через tpl-шаблонизатор — он всегда должен выводиться в готовом виде, часто после отдельного парсера текста. То есть контент — это всегда готовый html-код, даже если в контенте можно выполнять php-код. На выходе — всегда HTML.
Таким образом структура шаблона получается намного сложней, чем просто набор файлов. Но важно, что шаблон всегда принимает контент для вывода. По сути это единственное, что ему и нужно и это было бы замечательно, если бы не множественные настройки пользователя.
Шаблон сайта управляется через опции конфигурации. Эти опции могут быть на уровне контента, на уровне сайта, плагинов, самого шаблона, компонентов шаблона, да и просто меняться автоматом от различных условий. То есть шаблон должен быть устроен так, чтобы уметь меняться в зависимости от конфигурации.
Усугубим. Шаблон — это всегда html-структура. Принципиально есть три базовые части: начало HTML с секцией HEAD. Есть секция с начала BODY и есть секция в конце BODY/HTML.
Первая часть HEAD используется для подключения стилей, скриптов, разных meta и т.п. Последняя часть служит для lazy-подключений, а вот средняя часть представляет собой место для вывода контента. Но здесь всё не так просто.
Контент выводится не просто так, а в рамках модульной сетки. Она будет зависеть от множества условий, например если выводится сайдбар, то сетка будет другая. То есть здесь всё сильно завязывается на html.
Таким образом шаблон сайта
- должен быть отдельной сущностью в рамках системы и легко переключаться пользователем;
- должен иметь разные модульные сетки;
- должен управляться через опции любой части системы;
- должен хранить все те tpl-файлы, которые завязаны на дизайн сайта.
В качестве примера приведу два противоположных подхода к созданию шаблонов. Первый от WordPress, который не менялся ещё со времён b2. Его алгоритм всегда строится на том, что система сама заранее готовит данные для вывода, а шаблон просто проходится по готовому массиву и обрамляет элементы в html-структуру.
Вебмастер фактически никак не может повлиять на этот алгоритм, поэтому все шаблоны WordPress просто обречены на высокое ресурсопотребление и невозможность полноценной кастомизации. Элементарные опции, которые можно было бы вынести в админку — просто так сделать не получится. Все те шаблоны, которые имеют свои опции в админке — все они построены на обходных манёврах.
То есть суть шаблонов сайтов на подобии архитектуры WordPress в том, что шаблон представляется не просто как конечная часть вывода, а часть, которая очень жёстко завязана на черный ящик самой системы. Фактически разработчики CMS находятся в позиции, когда не допускают шаблонописателей ни к админке, ни к управлению работой системы.
Противоположный подход, который реализован в MaxSite CMS — максимальная гибкость и отсутствие лишней работы. В MaxSite CMS принято, что шаблон, хоть и часть системы, но он сам решает что и как ему нужно работать. Именно поэтому получение данных для вывода происходит на уровне шаблона, но система предлагает уже готовые решения. То же самое касается и интеграции в админ-панель. Система самостоятельно находит нужные для подключения файлы так, что вебмастеру даже не нужно заботиться о дополнительных действиях.
Именно поэтому в MaxSite CMS очень много настроек почти для всего. Можно настраивать виджеты, плагины, сам шаблон, его отдельные блоки, модули, компоненты, то есть вообще всё что угодно. Поэтому шаблон MaxSite CMS всегда имеет на порядок больше возможностей.
Если выбирать подход к построению шаблона, то гибкий шаблон (как в MaxSite CMS) более предпочтителен, поскольку с его помощью можно построить любой сайт, но при этом в нём намного больше сложной логики. Поскольку нужно учитывать значение многочисленных опций. Жёсткий подход (как в WordPress) намного проще для типового вывода, но при этом в нём практически невозможно реализовать нормальную настройку через опции.
Лично я пришёл к пониманию полноценного устройства шаблонов для MaxSite CMS не сразу. За 16 лет в системе шаблоны менялись несколько раз. От изначально простых (вроде Clouds, если кто помнит), до MF, который работает как самолёт 6-го поколения. Проблема здесь в том, что задачи по шаблону появляются не сразу — это эволюционный процесс. В какой-то момент ты понимаешь, что в текущих рамках развиваться дальше нельзя. Не всегда это зависит и от нас. Помнится был достаточно болезненный переход от Internet Explorer к FireFox и Chrome. А это потребовало отказа от большого количества кода.
Угадать тенденции очень сложно. В качестве примера приведу css-препроцессор Less, который показывал очень динамичное развитие, но отсутствие нормального порта на PHP или хотя бы собственный «сервер» для компиляции, всё свели на нет. В итоге доминирует Sass.
Многие вещи сейчас доступны в CSS и HTML, но раньше для этого приходилось изобретать костыли и велосипеды. Таков прогресс, но это порождает проблему совместимости. Если система перестает работать со старыми шаблонами, то это серьёзное неудобство для пользователей.
На своём опыте скажу, что процентов 30 кода в шаблоне MF и MaxSite CMS — это ради совместимости со старыми разработками.
Стратегические ошибки часто мешают дальнейшему развитию. Я мого раз жалел, что в качестве конфигурационных файлов взял ini-файлы. По сути они очень хороши, намного лучше json, yaml и им подобным. Но оказалось, что конфигурация может быть динамической. Например значение опции может состоять из списка файлов какого-то каталога. То есть формат файла конфигурации должен уметь работать с PHP. Поэтому мне пришлось придумывать несколько костылей, чтобы научить ini-файлы работать с нативным PHP.
Всего этого не было бы, если бы я сразу взял за основу обычный php-файл. Да, синтаксис массива более сложен, чем ini-файл, но возможностей было бы гораздо больше.
Из всего этого следует простой вывод. Нельзя ограничивать разработчика шаблона разного рода форматами и условностями. Идеальный вариант — обычный php-код, который работает абсолютно на любом уровне системы.
Когда разработчики своих проектов поймут эту простую истину, то это позволит им выкинуть огромный пласт кода, который не делает ничего полезного, а только ограничивает пользователя системы. Идеальная система такая, которая позволяет делать вебмастеру что угодно на уровне шаблона, а редактору сайта на уровне контента.
Теперь о технических задачах, которые стоят перед разработкой системы шаблонов сайта.
Самая главная проблема — это решить на каком этапе подключать модульную сетку сайта. Под этим я понимаю некий (условно) html-каркас, внутрь которого вставляется html-контент сайта (текст статьи по простому).
Нужно сразу учесть, что контент может сопровождаться дополнительным выводом. Например заголовок статьи может быть отдельным полем в базе данных. Рубрики, метки, изображение записи и т.п. То есть сам контент может быть выведен в рамках одной модульной сетки, но с разным дополнительным выводом.
Дальше. Контент должен влиять на секцию HEAD. Например для записи нужно подключить стороннюю библиотеку js и css-файлов. Или добавить какой-то meta-тэг.
Таким образом модульная сетка должна как-то уметь вначале получить все данные контента, и только потом построить секцию HEAD. То есть получается, что изначально нам нужно получить контент (для простоты считаем, что он идёт сразу со всеми своими опциями), а только потом подключить файл модульной сетки.
Сложность такого варианта в том, что в контенте может также выполняться php-код. Фактически это означает, что контент это не просто текст, а это php/html-код. Вначале нужно выполнить текст записи, как обычный PHP, потом его прогнать через парсер и этот результат уже доставлять в модульную сетку для вывода.
Технически это реализуется через буферизацию вывода и здесь есть два подхода. Первый буферизация включается для всего-всего вывода на уровне системы. Этот подход используется например в CodeIgniter и когда-то он был оправдан, поскольку упрощал поддержку header и т.п. Но это плохая практика. Поэтому на сегодняшний день лучший вариант — это буферизация только той части кода, которая действительно в этом нуждается. Например для выполнения контента.
На самом деле есть ещё и третий вариант — это отсутствие буферизации как таковой. Это происходит если контент подключается и выполняется уже внутри модульной сетки. Такой шаблон сепарирует контент на две части. Первая часть — это опции и все настройки, которые могут быть использованы до подключения модульной сетки. Вторая часть — это сам контент, который представляет собой не что иное, как php-код/файл.
Таким образом модульная сетка получает всё что ей нужно до вывода контента, а контент выполняется ровно так, как считает нужным. При этом все эти части никак не связаны между собой, разве что модульная сетка является обязательной для вывода.
Именно такой подход я использую в Albireo CMS. Кратко алгоритм будет таким:
- вначале формируются все опции и настройки страницы;
- у страницы указывается какой файл модульной сетки она хочет использовать (например с сайдбаром);
- подключается файл модульной сетки;
- устройство этого файла может быть любым, но в нужной части html-разметки мы просто подключаем php-файл страницы;
- внутри модульной сетки используем опции, которые указывают что нужно ещё подключить.
Преимуществом такого варианта будет то, что шаблон получается очень компактными. Как ни крути, а самая вариативная часть — это именно вывод самого контента — здесь мы указываем css-классы под разный дизайн. Но часть HEAD, начало и конец BODY будет един для всех шаблонов. Поэтому фактически верстка модульной сетки сводится только к вёрстке вывода контента. На практике это означает, что шаблон может иметь множество разных сеток/дизайнов для вывода.
Теперь кратко о понятии контента. Здесь нужно уточнение, поскольку под этим часто понимают только текст записи, но я рассматриваю это понятие более широко.
Рассмотрим пример. Есть страница сайта, которая выводит просто текст. В этом случае шаблон просто выводится как есть с помощью require (для файлов, или eval для баз). Но если мы хотим вывести сразу несколько записей?
Традиционно это решается с помощью больших заморочек с типом контента. Для этого создаются отдельные контролёры, модули, вьюшки и т.п. Всё это очень жестко.
В какой-то момент я осознал, что всё это просто не нужно. Если мы позволяем разместить в тексте записи php-код, то этот код может вывести любые записи по любым критериям. Очень необычный подход, но только в рамках закостеневшего старого подхода.
В реальности всё выглядит крайне просто. Например есть страница сайт/dinosauria, где я хочу вывести все записи с рубрикой dinosauria. Приведу полностью готовый код для Albireo CMS, который размещается прямо в тексте записи.
$files = getPagesField('category', 'dinosauria'); if (!$files) { header('HTTP/1.0 404 Not Found'); echo '<h1>Ничего не найдено...</h1>'; // или подключить tpl-файл } else { $pag = paginationA($files, limit: 7); // кол-во записей на одной странице пагинации echo '<h4 class="mar10-t mar30-b">Рубрика Динозавры</h4>'; echo tpl(data: $pag['files'], tpl: TPL_DIR . '1col2.php'); echo tpl(data: $pag, tpl: TPL_DIR . 'pagination1.php'); }
То есть в рамках шаблона просто не существует никаких типов страниц — шаблон выводит только данные, которые формирует страница. Если я захочу поменять адрес страницы на любой другой, то это не потребует никакого изменения внутреннего кода.
Если мне нужно будет изменить сам вывод, то работа будет происходить в tpl-файле (он часть шаблона). Для полноты примера приведу tpl-файл пагинации:
{@if $max > 1} {@foreach $pagLinks as $url => $num} {@if $url == 'current'} <span class="t90 b-inline pad15-rl pad7-tb rounded mar5-r mar10-b bg-gray600 t-white" style="cursor: default">{{ $num }}</span> {@else} <a class="t90 b-inline pad10-rl pad5-tb rounded mar5-r mar10-b hover-no-underline bg-gray100 hover-bg-gray700 t-gray700 hover-t-gray50" href="{{ $url }}">{{ $num }}</a> {@endif} {@endforeach} {@endif}
А также вывод цикла записей:
<div class="{@class ['mar30-b pad30-b b-clearfix', 'bor-gray300 bor2 bor-dotted-b' => !$loopLast]}"> h1(t180 mar10-b) <a href="{{ $page_url }}">{* $title *}</a> {@isset $announce} __(t90 mar10-tb) {{ $announce }} <a class="t-nowrap" href="{{ $page_url }}">Читать ➝</a> {@endisset} </div>
Только вдумайтесь, насколько проще становится работа с любым выводом данных. То есть контент это не только текст записи, но и любой другой вывод.
Для меня ещё очень важно дать пользователю работать с любыми адресами на сайте. Это Ахиллесова пята моей MaxSite CMS, когда я вынужден был завязываться на предопределённые шаблоны URL. Если рубрика, что category, если метка, то tag, если запись, то page. Но в Albireo CMS (да и текущем Albireo Framework) адрес страницы может быть абсолютно любым. К адресу вообще ничего не привязывается, ну разве что к 404-странице — она предопределена. А так, хоть рубрики, записи, метки, любые другие группировки записей (в Albireo CMS — они тотально произвольны) могут быть с любыми адресами.
Попробуйте сделать что-то подобное в рамках существующих php-фреймворков! Хрен там.
Слава Украине! Смерть рашистам!