Начиная с 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) большого количества шаблонов, при сохранении приемлемого быстродействия. Поэтому с радостью почитаю о нюансах и подводных камнях такого подхода от имеющих опыт людей в комментариях.
