Начиная с php 5.3, мы получили замечательную возможность использовать замыкания и анонимные функции. Они, вместе с альтернативным синтаксисом, очень удобны для использования при шаблонизации (конечно, за исключением случаев, когда верстальщику не надо давать доступ к php), а шаблоны на их основе быстры, легко переводятся в байткод акселератором, могут поддерживать блочное наследование, не требуют компиляции и кеширования, поддерживают скины и очень удобны в разработке.
Предполагается, что читатель имеет опыт работы с шаблонизаторами, например twig. Подробности под катом.
Для начала определимся с семантикой и синтаксисом.
1) Лямбда-функции в шаблоне назовем инструкциями, и будем писать их прописными буквами;
2) Системные и служебные переменные в шаблоне начинаются с символа подчерквания;
3) Все остальные переменные в шаблоне являются его непосредственными агрументами и пишутся со строчной буквы.
Шаблонизатор является объектом-сервисом, имеющем метод рендера:
В неймспейсе этого метода и будет происходить всё самое интересное.
Для начала, определим переменные и объявим в нём несколько инструкций с замыканиями, в конце добавим обработку результата:
Наследование и переопределение блоков работает следующим образом:
1) Если в начале шаблона задана инструкция $EXTEND(), то расширяемый шаблон задается «предком» текущего шаблона.
2) При начале наследуемого блока ("$BEGIN()") открывается буфер записи. Имя в параметре нужно только для семантики;
3) При завершении наследуемого блока буфер записи выбрасывается в аутпут, или же выбрасывается существующий запомненный буфер, если он был заполнен (здесь нюанс: выполняются оба блока — и предка, и потомка, но выводится один; на практике это не существенно);
Таким образом, сначала рендерится потомок, потом предок. Если в предке есть блоки, определенные в потомке, то они не выводятся, а заменяются блоками потомка. При этом скин предка берется из скина потомка (задается при вызове рендера). Могут быть более двух уровней связи «предок-потомок», в результате функции рендера будет только последний блок с заданным именем.
Инструкции $V, $L и $GV принимают только переменные (с использованием разрешения на изменение "&", которое нужно для того, чтобы исключить предупреждения обрщения к несуществующей переменной; переменная внтури лямбда-функции будет равна null)
Инструкция $R предназначена для вывода ресурсов, привязанных к шаблону. Например так:
Дополнительные инструкции (например, работа с константами и т.д.) могут быть переданы в метод через extract($this->instructions), этот массив лямбда-функций формируется динамически.
Пример шаблона (class используется в теге html с целью указания в одном из скинов height: 100%, например):
Пример использования шаблонизатора при сборке css:
Здесь $C — инструкция для работы с константами. Благодаря шаблонизации, в стилевых файлах можно делать циклы, встраивать функции расчета координат, расширять css-ки, инклудить на стороне сервера, и многое другое интересное. Стили начинаются с точки, потому что сборщик css заменяет точку в начале строку на селектор блока шаблона автоматически, таким образом поддерживается блочная верстка.
Точно так же можно пропускать через шаблонизатор javascript-файлы при сборке компонентов проекта. Например, это удобно для подстановки путей к ajax-запросам и прочим линкам с помощью инструкции $PATH('route_name',$args).
Все ресурсы шаблонов должны быть собраны сборщиком в папки докрута, а все css-ки и js-ки шаблнов предворены префиксными селекторами (см. "$CLASS()") и склеены в один (ну, два) файла в докруте. Об этих механизмах я расскажу отдельно в других статьях.
Отмечу, что данный подход существует в прототипе в рамках работающей системы, которую я целиком, по понятным причинам, в статье описывать не буду, и является эксперементальным. Основной целью было сокращение потерь времени на разработку из-за долгой компиляции (twig) большого количества шаблонов, при сохранении приемлемого быстродействия. Поэтому с радостью почитаю о нюансах и подводных камнях такого подхода от имеющих опыт людей в комментариях.
Предполагается, что читатель имеет опыт работы с шаблонизаторами, например twig. Подробности под катом.
Для начала определимся с семантикой и синтаксисом.
1) Лямбда-функции в шаблоне назовем инструкциями, и будем писать их прописными буквами;
2) Системные и служебные переменные в шаблоне начинаются с символа подчерквания;
3) Все остальные переменные в шаблоне являются его непосредственными агрументами и пишутся со строчной буквы.
Шаблонизатор является объектом-сервисом, имеющем метод рендера:
public function exec($_template,array $_data=array(),$_skin=null,$_type='php',&$_buffer=null) {
}
В неймспейсе этого метода и будет происходить всё самое интересное.
Для начала, определим переменные и объявим в нём несколько инструкций с замыканиями, в конце добавим обработку результата:
public function exec($_template,array $_data=array(),$_skin=null,$_type='php',&$_buffer=null) {
if (!isset($_skin)) $_skin = $this->api->cfg['default_skin'];
if (!$_filename = $this->getFile($_template,$_skin,$_type)) return '';
$_parent = null;
$_api = $this->api;
// Включает в шаблон ссылку на имеющий к нему отношение ресурс
$R = function($name) use ($_template, $_skin) {
echo "/res/t/{$_skin}/{$_template}/$name";
};
// Начинает наследуемый блок шаблона
$BEGIN = function($blockname) {
ob_start();
};
// Заканчивает наследуемый блок шаблона
$END = function($blockname) use (&$_buffer,$_parent) {
if (isset($_buffer[$blockname])) {
ob_end_clean();
echo $_buffer[$blockname];
} else {
$_buffer[$blockname] = isset($_parent)?ob_get_clean():ob_get_flush();
}
};
// Указывает имя расширяемого шаблона
$EXTEND = function($template,$type=null) use (&$_parent) {
if ($template) $_parent =array($template,$type);
};
// Включает другой шаблон внутрь шаблона
$INCLUDE = function($template,$type=null) use ($_data,$_api,$_skin) {
if ($template) echo $_api->templater->exec($template,$_data,$_skin,$type);
};
// Генерирует css-класс для оберточного dom-элемента
$CLASS = function() use ($_template,$_skin) {
echo "t-{$_template} s-{$_skin}";
};
if (!isset($this->instructions)) $this->instructions = $this->getInstructions();
// Вставляет переменную или дефолтное значение
$V = function(&$var,$default='',$raw=false) use ($api) {
if (isset($var)) {
if (is_scalar($var)) echo $raw?$var:htmlspecialchars($var);
else $api->templater->dump($var);
} else {
echo $raw?$default:htmlspecialchars($default);
}
};
// Возвращает переменную или дефолтное значение
$GV = function(&$var,$default='') {
if (isset($var)) return $var;
else return $default;
},
extract($_data);
// Немного магии - работа с языко-зависимыми строками (массив строк, ключом является язык)
if (!isset($_language)) $_language = $this->api->cfg['default_language'];
$L = function(&$stringhash,$default=array('?')) use ($_language,$_api) {
if (!isset($stringhash)) $stringhash = $default;
if (is_string($stringhash)) {
echo htmlspecialchars($stringhash);
return;
}
if (isset($stringhash[$_language[0]])) echo htmlspecialchars($stringhash[$_language[0]]);
elseif (isset($stringhash[$_api->cfg['default_language'][0]])) echo htmlspecialchars($stringhash[$_api->cfg['default_language'][0]]);
else echo htmlspecialchars(reset($stringhash));
};
extract($this->instructions);
ob_start();
include $_filename;
$content = ob_get_clean();
if ($_parent) $content = $this->exec($_parent[0],$_data,$_skin,$_parent[1],$_buffer);
return $content;
}
Наследование и переопределение блоков работает следующим образом:
1) Если в начале шаблона задана инструкция $EXTEND(), то расширяемый шаблон задается «предком» текущего шаблона.
2) При начале наследуемого блока ("$BEGIN()") открывается буфер записи. Имя в параметре нужно только для семантики;
3) При завершении наследуемого блока буфер записи выбрасывается в аутпут, или же выбрасывается существующий запомненный буфер, если он был заполнен (здесь нюанс: выполняются оба блока — и предка, и потомка, но выводится один; на практике это не существенно);
Таким образом, сначала рендерится потомок, потом предок. Если в предке есть блоки, определенные в потомке, то они не выводятся, а заменяются блоками потомка. При этом скин предка берется из скина потомка (задается при вызове рендера). Могут быть более двух уровней связи «предок-потомок», в результате функции рендера будет только последний блок с заданным именем.
Инструкции $V, $L и $GV принимают только переменные (с использованием разрешения на изменение "&", которое нужно для того, чтобы исключить предупреждения обрщения к несуществующей переменной; переменная внтури лямбда-функции будет равна null)
Инструкция $R предназначена для вывода ресурсов, привязанных к шаблону. Например так:
<?foreach($GV($pictures,array()) as $picture):?>
<img src="<?$R($picture)?>"/>
<?endforeach;?>
Дополнительные инструкции (например, работа с константами и т.д.) могут быть переданы в метод через extract($this->instructions), этот массив лямбда-функций формируется динамически.
Пример шаблона (class используется в теге html с целью указания в одном из скинов height: 100%, например):
<!DOCTYPE html>
<html class="<?$CLASS()?>" id="<?$V($_id)?>">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title><?$L($title,array('en'=>'No title','ru'=>'Нет заголовка'))?></title>
<link rel="stylesheet" href="/res/var/t.css?<?=$_api->build?>" type="text/css" media="all" />
<script type="text/javascript" src="/res/jquery.js?<?=$_api->build?>"></script>
<script type="text/javascript" src="/res/var/t.js?<?=$_api->build?>"></script>
<script type="text/javascript" src="/res/var/frontend.js?<?=$_api->build?>"></script>
</head>
<body>
<header>
<?$BEGIN('header')?>
<h1><?$L($title,array('en'=>'No title','ru'=>'Нет заголовка'))?></h1>
<?$SLOT($langswitch)?>
<?$END('header')?>
</header>
<section>
<?$BEGIN('content')?>
<?$L($content,array('en'=>'No content','ru'=>'Нет контента'))?>
<?$END('content')?>
</section>
<footer>
<?$BEGIN('footer')?>
<?$SLOT($menu_footer)?>
<p class="copy">
<?=date('Y')?> MyProject
</p>
<p class="info">
<?$s=array('en'=>'Generated','ru'=>'Создано');$L($s)?>: <?=date('r')?>
</p>
<?$END('footer')?>
</footer>
</body>
</html>
Пример использования шаблонизатора при сборке css:
. {background: url("<?$R('bg.png')?>")}
. > .left {width: <?$C('left-margin')?>; border: 1px <?$C('border-color')?> solid;}
Здесь $C — инструкция для работы с константами. Благодаря шаблонизации, в стилевых файлах можно делать циклы, встраивать функции расчета координат, расширять css-ки, инклудить на стороне сервера, и многое другое интересное. Стили начинаются с точки, потому что сборщик css заменяет точку в начале строку на селектор блока шаблона автоматически, таким образом поддерживается блочная верстка.
Точно так же можно пропускать через шаблонизатор javascript-файлы при сборке компонентов проекта. Например, это удобно для подстановки путей к ajax-запросам и прочим линкам с помощью инструкции $PATH('route_name',$args).
Все ресурсы шаблонов должны быть собраны сборщиком в папки докрута, а все css-ки и js-ки шаблнов предворены префиксными селекторами (см. "$CLASS()") и склеены в один (ну, два) файла в докруте. Об этих механизмах я расскажу отдельно в других статьях.
Отмечу, что данный подход существует в прототипе в рамках работающей системы, которую я целиком, по понятным причинам, в статье описывать не буду, и является эксперементальным. Основной целью было сокращение потерь времени на разработку из-за долгой компиляции (twig) большого количества шаблонов, при сохранении приемлемого быстродействия. Поэтому с радостью почитаю о нюансах и подводных камнях такого подхода от имеющих опыт людей в комментариях.