Легкий шаблонизатор на PHP
Вниманию читателей предлагается простой в использовании обработчик шаблонов на языке PHP с поддержкой условных конструкций, циклов, подключения вложенных шаблонов и вызова функций. Время обработки шаблона составляет в среднем 20-70 мс. Для работы требуется подключение одного php-файла (есть и на GitHub). Код стабилен и применяется на нескольких работающих проектах.
Основы синтаксиса: переменные, условия, циклы
Перейдём сразу к практике и посмотрим, как выглядит шаблон, взяв для примера немного упрощенную версию HTML-кода сайта webew.ru:
<head>
/* Многострочные комментарии - как в C или PHP */
<title>{*title*}</title>
<base href="{*=BASEHREF*}"> /* константа */
/* условные конструкции: */
{?*keywords*}
<meta name="keywords" content="{*keywords*}">
{?}
{?*description*}
<meta name="description" content="{*description*}">
{?}
/* условие с негативной частью */
<link rel="stylesheet" href=" {?*default_css*} default.css {?!} special.css {?} ">
</head>
<body>
<div id="logo">
<a href=""><img src="{*logo.image*}" alt="{*logo.alt*}"></a>
</div>
<div id="menu">
/* цикл: */
{%*menu*}
<a href="{*menu:url*}">{*menu:title*}</a>
{%}
</div>
<div id="content">
{*content*}
</div>
<div id="footer">
webew.ru © {*year*}
</div>
</body>
</html>
А вот соответствующий код PHP:
ini_set('pcre.backtrack_limit', 1024*1024); // (см. ниже)
$DATA['title'] = 'Webew: теория и практика веб-технологий ';
define('BASEHREF', 'http://webew.ru/');
$DATA['logo'] = array(
'image' => 'i/logo.gif',
'alt' => 'Логотип webew.ru'
);
$DATA['menu'] = array(
array('url' => 'css', 'title' => 'CSS'),
array('url' => 'php', 'title' => 'PHP'),
array('url' => 'seo', 'title' => 'Интернет-маркетинг'),
array('url' => 'c', 'title' => 'C/C++')
);
$DATA['content'] = 'Приветствуем вас на webew.ru!';
require_once 'websun.php'; // подключаем файл с шаблонизатором
$tpl = 'templates/main.tpl'; // путь к шаблону
$html = websun_parse_template_path($DATA, $tpl); // запуск шаблонизатора
echo $html; // получили обработанный шаблон, отдаем клиенту результат
Несмотря на то, что переменные $DATA['keywords'] и $DATA['description'] не установлены, при обработке шаблона не возникнет никаких ошибок или предупреждений.
Условная конструкция вида {?*keywords*} что-то {?} означает «вставить что-то, если в переменной, переданной шаблону (в данном случае $DATA) присутствует элемент keywords и при этом он не является пустой строкой, нулём, FALSE, NULL или пустым массивом». Соответственно, конструкция {?!*keywords*} ... {?} срабатывает, если какое-нибудь из этих условий не выполняется.
Шаблонизатор не генерирует ошибок или предупреждений в том случае, если какой-либо из запрошенных переменных или констант вообще не существует (вместо них молчаливо будет вставлена пустая строка) поэтому формат передаваемых данных может быть нестрогим.
В цикле ключи массива указываются через двоеточие — {*menu:title*}, а вне цикла через точку — {*logo.image*}.
Можно также обращаться к переменным глобальной области видимости, в т.ч. суперглобальным массивам. Для этого перед именем переменной нужно поставить знак доллара. Например, подстановка в шаблон переменной $_GET['foo'] выглядит так: {*$_GET.foo*}.
Вложенные шаблоны
Обычно код бывает удобно разбить на несколько шаблонов, которые затем подключать по мере необходимости. Пусть шаблон меню находится в файле menu.tpl. Тогда код из главного шаблона
{%*menu*}
<a href="{*menu:url*}">{*menu:title*}</a>
{%}
</div>
в неизменном виде переместится в menu.tpl, а вместо него будет
<div id="logo">
...
</div>
{* +menu.tpl *} /* подключаем шаблон меню */
<div id="content">
...
Путь к шаблону может также передаваться в переменной. Например, если записать путь к шаблону меню в $DATA['menu_template'], то подключение его примет вид
Существует несколько способов указания путей к шаблонам.
Самый наглядный метод — это указание абсолютного пути к файлу шаблона в файловой системе — что-то наподобие
$tpl = '/home/webew.ru/htdocs/templates/main.tpl'
Аналогично, для подключения шаблона меню с помощью абсолютного пути приходится писать
{* +/home/webew.ru/htdocs/templates/menu.tpl *}
Если в начале пути к шаблону отсутствует слэш, то путь интерпретируется относительно месторасположения текущего шаблона (это особенно удобно для группы связанных шаблонов, которые становится легче читать, и к тому же можно все вместе переносить, не меняя при этом путей в них, если они записаны как относительные).
Обычно все шаблоны находятся в каком-то одном каталоге (или его дочерних) и шаблонизатору можно указать так называемый корневой каталог шаблонов — такой, относительно которого он будет интерпретировать пути к ним. Корневой каталог передается третьим аргументом:
$DATA,
$tpl,
'/home/webew.ru/htdocs/templates'
);
Сообщать шаблонизатору о том, что интерпретировать путь нужно относительно корневого каталога шаблонов, следует, поместив в начало пути символ ^:
$tpl = '^/main.tpl';
Можно указывать также путь к шаблону относительно корневого каталога веб-сервера (используется переменная $_SERVER['DOCUMENT_ROOT']). Для этого в начало пути нужно поместить знак доллара.
Таким образом, путь к одному и тому же шаблону (как при его подключении его из других шаблонов, так и при вызове websun_parse_template_path) можно указать четырьмя разными способами:
// корневой каталог шаблонизатора - /home/webew.ru/htdocs/templates
// тогда следующие четыре записи эквивалентны:
$tpl = 'menu.tpl'; // путь относительно текущего шаблона
$tpl = '^/menu.tpl'; // относительно корневого каталога шаблонизатора
$tpl = '$/templates/menu.tpl'; // относительно корневого каталога веб-сервера
$tpl = '/home/webew.ru/htdocs/templates/main.tpl'; // абсолютный путь для ФС
При запуске шаблонизатора (вызов функции websun_parse_template_path) и при подключении не связанных друг с другом шаблонов рекомендуется использовать путь относительно корневого каталога шаблонизатора, в остальных случаях как правило удобнее пользоваться относительными путями.
По соображениям безопасности список возможных расширений файлов шаблонов по умолчанию ограничен набором *.tpl, *.html, *.css, *.js, *.xml. Изменить этот набор можно, воспользовавшись объектной формой вызова шаблонизатора и передав ей параметр allowed_extensions.
Передача во вложенный шаблон части массива данных
Зачастую подключаемый шаблон имеет дело не со всем массивом данных, а только с какой-то его частью (например, в шаблоне меню не нужны никакие переменные, кроме $DATA[menu]). Записывать каждый раз префикс массива в таком шаблоне становится излишним и неудобным, код шаблона загромождается. Чтобы этого избежать, подключаемому шаблону можно передать не весь массив, а лишь нужный элемент.
Вот как будут выглядеть основной шаблон и шаблон меню при таком подходе:
(основной шаблон)
{* + *menu* | menu.tpl *}
/* путь к шаблону в таком случае указываем через вертикальную черту */
...
(или другой вариант — с передачей пути в переменной:)
Шаблон меню:
{%**}
<a href="{*:url*}">{*:title*}</a>
{%}
</div>
Конструкции вида {**}, {%**} и {?**} означают обращение к корневой переменной, переданной в шаблон. А запись вида {* + шаблон *} есть укороченная форма конструкции {* + ** | шаблон *} (** — в подключаемый шаблон передается вся корневая переменная).
Можно пойти еще дальше и вынести код для элемента меню в отдельный шаблон:
{%**} {* + *:* | menu-item.tpl *} {%}
</div>
Здесь конструкция *:* указывает при итерации на каждый из элементов корневого массива и означает передачу их в подключаемый шаблон. Такая запись имеет и сокращенный вариант:
menu-item.tpl:
Передача элементов в подшаблон ускоряет обработку и снижает объем используемой памяти, поэтому является рекомендуемой практикой, особенно при итерации по массиву.
Вызов функций и методов классов
Прямо из шаблонов можно вызывать функции PHP. Например:
Случайное число от 1 до $DATA['max']: {* @rand(1, *max*) *}.
Массив $DATA['list'], "склеенный" пробелом: {* @implode(" ", *list*) *}.
Литералы массивов следует передавать в виде JSON:
{* @implode( " ", [1,2,3] ) *}
{*
@print_r( {
"a":"b",
"с": [5, 6, 7],
"d": { "e": "f" }
} )
*}
Стандартный набор функций, которые можно вызывать из шаблона, ограничен следующим списком:
date
htmlspecialchars
implode
in_array
is_array
is_null
json_encode
mb_lcfirst
mb_ucfirst
rand
round
strftime
urldecode
var_dump
Можно зарегистрировать дополнительные. Для этого нужно создать в глобальной области видимости специальную переменную под названием $WEBSUN_ALLOWED_CALLBACKS:
'some_function',
'SomeNamespace\some_other_function',
'SomeClass::somemethod', // все методы классов определяем через двоеточие
'...'
);
При отсутствии вызываемой из шаблона функции среди зарегистрированных будет сгенерировано предупреждение.
Вызовы функций могут быть вложенными:
Результат работы функции передавать для подстановки в какой-нибудь другой шаблон, где этот результат будет принят в качестве корневой переменной. Например:
Имя функции можно также передавать шаблону в переменной. Например, если $DATA['rand_function'] = 'rand' и $DATA['rand_template'] = 'rand-block.tpl', то вызов будет выглядеть так:
Можно вызывать статические методы классов, а также методы экземляров объектов:
{* SomeClass::someMethod(*somedate*) *} - вызов статического метода класса
Регистрировать среди разрешенных к вызову следует по имени класса и метода:
'DateTime::format',
'SomeClass::someMethod'
];
Скачать
Чтобы начать использовать шаблонизатор, достаточно скачать и подключить его php-файл.
Дополнительные возможности
Ниже приводится некоторая дополнительная информация, которая может быть полезной при работе с шаблонизатором.
Помимо websun_parse_template_path(), принимающей путь к шаблону, есть еще функция websun_parse_template, которой вместо пути шаблона передается его содержимое:
$template = '
{*keywords*}
...
';
$html = websun_parse_template(
$DATA,
$template,
'/home/webew.ru/htdocs/templates' // можно не указывать
);
(на самом деле обработчик шаблонов реализован в виде объекта, создание и настройка которого для удобства инкапсулированы в функции websun_parse_template и websun_parse_template_path; если интересно, как это происходит, можно посмотреть код этих функций)
Условия можно конструировать с использованием сравнения:
{?!* a = 1 *} a не равно 1 {?}
{?* a != 1 *} a не равно 1, другой способ {?}
{?* a == 1 *} a строго равно 1 (числу) {?}
{?* a == "1" *} a строго равно 1 (строке) {?}
{?* a !== 1 *} a строго не равно 1 (числу) {?}
{?* a < 1 *} a меньше 1 {?}
{?* a > 1 *} a больше 1 {?}
{?!* a > 1 *} a не больше 1 {?}
{?* a >= 1 *} a больше либо равно 1 {?}
{?* a = b *} а равно b {?}
{?* a = "раз" *} a - это "раз" {?}
{?* a = TRUE *} а истинно (с приведением типа) {?} (в таком случае лучше писать просто {?*a*}...{?} - эффект будет тот же )
{?* a == TRUE *} а строго истинно {?}
{?* a == true *} можно как в верхнем, так и в нижнем регистре {?}
а не равно 1
{?* a = 10 *} а равно 10 {?}
{?* a = b *} а равно b {?}
{?}
{?* @array_sum(*a*) > 15 *} сумма элементов a больше 15 {?}
{?* @array_sum(*a*) > @array_sum(*b*) *} сумма элементов a больше суммы элементов b {?}
и т.п.
Часть после знака сравнения интерпретируется как имя переменной, если только она не заключена в двойные кавычки (в этом случае для сравнения берется строка внутри кавычек) или же не состоит полностью из цифр (тогда берется соответствующее число).
Для ясности следует отметить, что условные конструкции можно использовать и внутри циклов. Например, можно выделить активный пункт меню, поместив его в <strong> вместо обычных ссылок:
array('url' => 'css', 'title' => 'CSS'),
array('url' => 'php', 'title' => 'PHP', 'active' => TRUE), // активный пункт меню
array('url' => 'seo', 'title' => 'Интернет-маркетинг'),
array('url' => 'c', 'title' => 'C/C++')
);
Шаблон:
{?!*menu:active*}<a href="{*menu:url*}">{*menu:title*}</a>{?}
{?*menu:active*}<strong>{*menu:title*}</strong>{?}
{%}
Следует отметить, что, несмотря на то, что для конкретной записи элемент 'active' может быть вообще не установлен, при работе шаблонизатора PHP не будет генерировать предупреждений или замечаний (т.е. засорять журнал ошибок), поскольку в шаблонизаторе для таких случаев есть в т.ч. проверка с помощью isset().
Циклы могут содержать негативную часть, подобно условиям. Она исполняется в случае, если переменная цикла содержит пустое значение или не установлена:
(список)
{%!}
Список пуст!
{%}
Иногда сократить запись можно с помощью конструкции вида {*var1|var2|var3|"раз"*}, которая заменяется на первую из перечисленных непустую альтернативу (в данном примере, если все переменные окажутся пустыми, будет подставлена строка "раз").
С помощью | можно также объединять в OR несколько условий:
var_1 равно "one" или var_2 равно "two"
{?}
А с помощью & - в AND:
var_1 равно "one" и при этом var_2 равно "two"
{?}
При использовании PHP 5.2.2 и выше следует обратить внимание на настройку pcre.backtrack_limit, которая ограничивает максимальный размер строки, обрабатываемой функциями семейства preg (по умолчанию это 100 Кб). Характерный признак недостаточно большой величины pcre.backtrack_limit — получение пустой строки в результате обработки шаблона в случае, когда этого быть не должно. Избежать этого можно, увеличив это значение (лучше с большим запасом — к растрате ресурсов это не приведет):
Итерация по одномерному массиву (вида $DATA['list'] = array(10,20,30,40,50)) делается очень просто. Например, вывести его элементы по одному в строке можно так:
{%*list*} {*list:*}<br> {%}А вообще можно использовать массивы любой размерности (не только двумерные), а циклы строить — любой вложенности:
array(
'id' => 1,
'title' => 'Москва',
'codes' => array(77, 97, 99, 177, 197),
'neighbours' =>
array('id' => 25, 'title' => 'Калуга'),
array('id' => 32, 'title' => 'Тула'),
array('id' => 47, 'title' => 'Владимир'),
array('id' => 59, 'title' => 'Рязань'),
array('id' => 134, 'title' => 'Тверь'),
array('id' => 15, 'title' => 'Смоленск'),
array('id' => 37, 'title' => 'Ярославль')
),
),
array(
'id' => 2,
'title' => 'Санкт-Петербург',
'codes' => array(78, 98),
'neighbours' =>
array('id' => 69, 'title' => 'Петрозаводск'),
array('id' => 78, 'title' => 'Вологда'),
array('id' => 10, 'title' => 'Новгород'),
array('id' => 117, 'title' => 'Псков')
),
...
);
Шаблон:
<h1>{*cities:title*}</h1>
<p>
Автомобильные коды:
{%*cities:codes*}
{*cities:codes:*} /* список кодов - одномерный массив */
{%}
</p>
<p>
Соседние регионы:
<br>
{%*cities:neighbours*}
<a href="/city/{*cities:neighbours:id*}">
{*cities:neighbours:title*}
</a>
<br>
{%}
</p>
{%}
К элементам такого массива можно обращаться и вне цикла, например, {*cities.0.neighbours.1.title*} — 'Тула' и т.п.
Внутри цикла есть также возможность обратиться к ключу массива - с помощью инструкции ^KEY, например:
{%*cities*}№ {*cities:^KEY*}. {*cities:title*}<br>
{%}
Аналогично, ^i и ^N внутри цикла — порядковый номер элемента в массиве, начиная с 0 и 1 соответственно.
А с помощью ^COUNT можно оперировать числом элементов в массиве:
(перед ^COUNT нет двоеточия, т.к. его можно использовать и вне циклов)
Подключение шаблонов внутри циклов можно переписать покороче:
{%*menu*} {* +*menu:* | menu-item.tpl *} {%}
/* но лучше - так (перед именем передаваемой в шаблон переменной появился знак %) */
{* +*%menu* | menu-item.tpl *}
Аналогичное сокращение действует и для подключения шаблона внутри условия:
{?*menu*} {* +*menu* | menu-item.tpl *} {?}
/* но лучше - так (перед именем переменной появился знак вопроса) */
{* +*?menu* | menu-item.tpl *}
Можно вызывать шаблон рекурсивно из самого себя, указав вместо пути специальную нотацию ^T. Это бывает полезно при вложении друг в друга идентичных структур (например, меню с иерархической структурой).
<a href="{*url*}">{*title*}</a>
{?*submenu*}
<div class="submenu">
/* здесь вызываем текущий шаблон для каждого из элементов подменю */
{* + *%submenu* | ^T *}
</div>
{?}
</div>
Будьте внимательны, избегайте бесконечных циклов!
Есть возможность передавать в переменной не путь к шаблону, а непосредственно его содержимое (иногда это бывает удобно). Пользоваться этим можно так же, как и обычными вложенными подшаблонами:
/* фактически дополняем переменной
текущий шаблон; так можно, грубо говоря,
сделать её содержимое динамическим
с помощью шаблонизатора */
{* + *subvar* | >*tpl* *}
/* можно и часть массива */
{* + *?subvar* | >*tpl* *}
/* если в subvar что-то есть.. */
{* + *%subvar* | >*tpl* *}
/* пройтись по переменной в цикле */
{* + *@module()* | >*tpl* *}
/* использовать переменную как шаблон для подстановки результатов работы функции */
Можно отключить возможность использовать в шаблоне переменные глобальной области видимости и суперглобальные переменные ($_GET, $_SERVER, $_COOKIES и пр.). Для этого нужно передавать функциям websun_parse_template_path() и websun_parse_template() четвертый аргумент, равный TRUE:
$DATA,
$template_path,
'/home/webew.ru/htdocs/templates',
TRUE
);
Все такие переменные при обработке шаблонов будут заменены на пустые строки.
© Все права на данную статью принадлежат порталу webew.ru. Перепечатка в интернет-изданиях разрешается только с указанием автора и прямой ссылки на оригинальную статью. Перепечатка в печатных изданиях допускается только с разрешения редакции.