4064 читателя, 405 постов
Администрация
Модераторы
Блог для обмена опытом
Примечание: дабы не плодить сущего, я не буду пересказывать статью про наследование шаблонов в Django, однако рекомендую её прочитать, дабы примерно понять, что нас ждёт и чтобы по исходным текстам шаблонов можно было понять, что они делаютВопреки расхожему мнению, одной из главных задач Smarty является не банальная замена
<? php echo $var ?> более лаконичными {$var}, а расширение базовой функциональности плагинами. В частности, Smarty позволяет определять собственные блоковые функции. Именно этим и воспользуемся.Примечание: в отличие от Django, здесь будет использован не одиночный тегСинтаксис шаблонов наследования будет примерно таким:{% extend %}, а блок{extends}...{/extends}, в пределах которого будут располагаться наследуемые блоки. Сделано это было, во-первых, из-за простоты реализации, во-вторых — этот подход даёт возможность наследовать разные шаблоны (хорошо это плохо — вопрос другой; в крайнем случае, никто не заставляет использовать несколько блоков{extends}в одном шаблоне).
parent.tpl:Особо, думаю, ничего пояснять не надо: перед компиляцией шаблона блок<html> <head> <title> Inherit it! </title> </head> <body> <p>Just a paragraph</p> <p>{block name="foo"}It's a parent{/block}</p> </body> </html>
child.tpl:{extends template="parent.tpl"} {block name="foo"}It's a child{/block} {/extends}
index.php:<?php $smarty->display('child.tpl'); ?>
{extends} заменяется содержимым шаблона, который указан в параметре template блока. Все именованные блоки, которые были определены внутри {extends}, перекрывают соответствующие блоки в родительском шаблоне.Идея вкратце такова: внутри объекта шаблонизатора введём ассоциативный массив, ключами которого будут имена наследуемых блоков, а соответствующими им значениями — массивы, содержащие текстовые содержания этих блоков, хранящиеся в порядке их (блоков) вызова. Согласен, фраза получилась заумной, поэтому проще показать на предыдущем примере:<html> <head> <title> Inherit it! </title> </head> <body> <p>Just a paragraph</p> <p>It's a child</p> </body> </html>
<code>Array
(
[foo] => Array
(
[0] => It's a parent
[1] => It's a child
)
)</code>Надеюсь, всё просто. Теперь остаётся при вызове блока в шаблоне «достать» из этого хранилища последний элемент и отобразить его на месте тегов :)extends и block, а так же ввести хранилище значений. {extends}{/extends} будет отвечать за получение исходного кода шаблона-родителя, а {block}{/block} — за создание и переопределение наследуемых блоков. <?php
/**
* Блок, наследующий шаблон
*
* @param array $params Список параметров, указанных в вызове блока
* @param string $content Текст между тегами {extends}..{/extends}
* @param mySmarty $smarty Ссылка на объект Smarty
*/
function smarty_block_extends($params, $content, mySmarty $smarty)
{
/** Никому не доверяйте. Даже себе! */
if (false === array_key_exists('template', $params)) {
$smarty->trigger_error('Укажите шаблон, от которого наследуетесь!');
}
return $smarty->fetch($params['template']);
}
?>
block.block.php:<?php
/**
* Создаёт именованные блоки в тексте шаблона
*
* @param array $params Список параметров, указанных в вызове блока
* @param string $content Текст между тегами {extends}..{/extends}
* @param mySmarty $smarty Ссылка на объект Smarty
*/
function smarty_block_block($params, $content, mySmarty $smarty)
{
if (array_key_exists('name', $params) === false) {
$smarty->trigger_error('Не указано имя блока');
}
$name = $params['name'];
if ($content) {
$smarty->setBlock($name, $content);
}
return $smarty->getBlock($name);
}
Здесь надо сказать, что setBlock() и getBlock() — методы шаблонизатора, которые соответственно помещают и получают текстовые значения наследуемых блоков из стека, про который было сказано выше. Расширим класс Smarty, введя массив стека и методы:<?php
class mySmarty extends Smarty
{
/**
* Список зарегистрированных блоков в шаблонизаторе
*
* @var array
*/
protected $_blocks = array();
/**
* Конструктор класса
*
* @param void
* @return void
*/
public function __construct()
{
$this->Smarty();
}
/**
* Регистрирует наследуемый блок шаблона
*
* @param string $key
* @param string $value
* @return void
*/
public function setBlock($key, $value)
{
if (array_key_exists($key, $this->_blocks) === false) {
$this->_blocks[$key] = array();
}
if (in_array($value, $this->_blocks[$key]) === false) {
array_push($this->_blocks[$key], $value);
}
}
/**
* Возвращает код блока согласно иерархии наследования
*
* @param string $key
* @return string
*/
public function getBlock($key)
{
if (array_key_exists($key, $this->_blocks)) {
return $this->_blocks[$key][count($this->_blocks[$key])-1];
}
return '';
}
}
?>mySmarty.class.php, можно создавать объект класса mySmarty и пользоваться прелестями наследования шаблонов.
комментарии (51)
Но спасибо за положительный отзыв :)
правда, обычно это делается не на уровне шаблонизатора.
например, для Drupal есть модуль cache router, который позволяет распределять кэшируемые объекты по разным хранилищам (файловая система, БД, memcached.
Вечером постараюсь дома написать топик на хабр со своим вариантом.
Спасибо за решение.
А насчёт критики — занятно. Все обычно акцентируют внимание на тормознутости и «интерпретируемый язык на интерпретируемом языке», про отсутствие наследования никто не вспоминал; я, если честно, вообще не знаю на PHP шаблнизаторов с реализованным уже наследованием. Они, похоже, не существуют или не получили широкого распространения.
Жёсткой необходимости нет почти ни в чём. Можно ведь и не заморачиваться на ООП в целом и Смарти в частности, а просто писать код линейно и потоком, не заморачиваясь на такие мелочи, как разделение логик приложения и отображения :). Вопрос в том, насколько потом ЭТО будет легко поддерживаться.
То же самое и с шаблонами. Я уже на собственном опыте убедился, что подобный подход проще в дальнейшем расширении. Особенно актуально для создания новых шаблонов на основе уже существующих :)
{container name=«layout.tpl»}
Something here…
{/container}
В layout.tpl вложенный код вставляется тэгом {$_output}
ЗЫ Не забудьте написать обработку за*loop*ленных включений шаблонов ;)
А насчёт «ЗЫ»: что такое за*loop*ленные включения шаблонов? Если это просто инклюд шаблона внутри
{loop}{/loop}или{foreach}{/foreach}, то, как мне кажется, проблем возникнуть не должно. Если ошибся — поправьте. Желательно с примерами :)А простой пример залупления :) — если вы напишете в файле index.tpl:
{include file=«index.tpl»}
В плагине наследования/контейнера может быть так, что в каком-то из родительских шаблонов обрабатывается снова какой-то из подшаблонов. Например, если в вашем parent.tpl написать:
{extends template=«child.tpl»}bla-bla-bla{/extends}
всё закончится очень печально. Цепочка подключений может быть длиннее, что затруднит поиск повторного инклуда. Ситуация смоделированная, но вполне реальная в условиях по-другому спроектированной обширной системы.
if (in_array($value, $this->_blocks[$key]) === false) { array_push($this->_blocks[$key], $value); }Вот этот момент, по-моему, лишний. Если я присвоил блоку значение, потом его переопределил, а потом хочу переопределить на предыдущее (хоть я этого, наверное, ни разу и не делал), то я хочу всё же получить последнее переопределенное значение. А так получается, что я могу написать:
parent.tpl:
Inherit it!
Just a paragraph
{block name=«foo»}It's a parent{/block}
{extends template=«parent.tpl»}
{block name=«foo»}It's a child{/block}
{/extends}
Вот этот момент, по-моему, лишний. Если я присвоил блоку значение, потом его переопределил, а потом хочу переопределить на предыдущее (хоть я этого, наверное, ни разу и не делал), то я хочу всё же получить последнее переопределенное значение. А так получается, что я могу написать:
<code>{block name=«foo»}It's a parent{/block}</code><code>{extends template=«parent.tpl»} {block name=«foo»}It's a child{/block} {/extends}</code><code>{extends template=«parent.tpl»} {block name=«foo»}It's a child{/block} {/extends}</code>Дело тут в том, что если эту проверку убрать, то в массив будет дампаться всё дерево наследования. И последним элементом, разумеется, будет «корень». Если непонятно, просто сделайте var_dump($template), увидите, что я имею в виду.
Я пошёл на это ограничение, исходя именно из тех соображений, что случаи, когда «внук» будет точно таким же, как его «дедушка», довольно редки и ими можно пренебречь :)
ведь конструкция типа
{$item->getColorById(1)->getSizeById(2)->name}
не работала :-(
знаю только что нужно регекспы патчить, но они там такие, что чёрт ногу сломит…
Хотя ходят слухи, что где-то существует приватная версия смарти, которая позволяет вызывать методы объектов цепочкой… ;)
для смарти здесь:
habrahabr.ru/blogs/php/45651/#comment_1159897
Приведу реальный пример из своей практики.
У меня есть класс Application. Он хранит сообщения об ошибках (например неправильно заполнена форма) во внутреннем массиве $_messages. Сообщения добавляются в массив следующим методом:
public function addMessage($text, $type = 'Error') { if (empty($text)) return true; if (empty($type)) $type = 'Error'; $this->_messages[$type][] = $text; return true; }А извлекаются следующим методом:
public function getMessages($type = 'Error') { if (empty($type)) { $type = 'Error'; } $aMessages = isset($this->_messages[$type]) ? $this->_messages[$type] : false; $this->_messages[$type] = null; return $aMessages; }А выводятся эти сообщения в шаблоне _errors.tpl, который инклудится в основном шаблоне layout.tpl, таким вот образом:
{assign var="aFailureMessages" value=$Application->getMessages('Error')} {assign var="aSuccessfulMessages" value=$Application->getMessages('Success')} {assign var="aInformativeMessages" value=$Application->getMessages('Informative')} {if $aFailureMessages || $aSuccessfulMessages || $aInformativeMessages} <table width="100%" cellspacing="0" cellspacing="0" border="0"> {if $aFailureMessages} <tr> <td class="error_box_red"> {section name=failure loop=$aFailureMessages} {$aFailureMessages[failure]} {/section} </td> </tr> <tr><td> </td></tr> {/if} {if $aSuccessfulMessages} <tr> <td class="error_box_green"> {section name=success loop=$aSuccessfulMessages} {$aSuccessfulMessages[success]} {/section} </td> </tr> <tr><td> </td></tr> {/if} {if $aInformativeMessages} <tr> <td class="error_box_blue"> {section name=informative loop=$aInformativeMessages} {$aInformativeMessages[informative]} {/section} </td> </tr> <tr><td> </td></tr> {/if} </table> {/if}Т.е. логика работы этого механизма такова, что если сообщение было выведено для просмотра значит его уже не нужно выводить.
Но каково было мое удивление, когда применяя приведенный здесь механизм наследования шаблонов, я не увидел ни единого сообщения, добавленного в Application.
В ходе отладки выяснилось, что метод getMessage вызывается по 2 раза на каждый тип сообщений, т.е. шаблон рендерится дважды!!!
Если посмотреть документацию smarty по блоковым функциям, коими являются smarty_block_extends и smarty_block_block, то они вызываются по два раза, при открытии и закрытии соответствующих тегов smarty {extends} и {block}.
В случае с smarty_block_extends дважды вызовется метод $smarty->fetch!!! вот тут собака и порылась. Таким образом, родительский шаблон рендерится дважды. Но это не оптимально — раз, и приводит к печальному результату с моими сообщениями — два.
Чтобы этого избежать, нужно рендерить родительский шаблон всего лишь один раз, когда у нас имеются данные для подстановки в теги {block}, когда был обработан шаблон-потомок, а именно во время второго вызова smarty_block_extends.
Внутри функции smarty_block_extends ставим проверку вида:
if (!is_null($content)) { return $smarty->fetch($params['template'], ...); } return false;Казалось бы, проблема решена, но! Во время переназначения содержимого блоков в таком случае данные push'атся в обратном порядке, т.е. содержимое блока из шаблона-наследника будет не на ВЕРШИНЕ стека, а на его ДНЕ. Т.е при получении содержимого блока при помощи метода getBlock, мы получим значение блока из родительского шаблона.
Решение: заменить в методе setBlock
на
лучше заменить на
end($this->_blocks[$key])
наверное
1. базовый шаблон загружается дважды (при открывающем теге extends и при закрывающем)
1. базовый шаблон загружается дважды (при открывающем теге extends и при закрывающем)
2. если вставляю два наследуемых блока, а внутренний блок расширяю только для одного, то для второго будет подставлено содержимое первого вставленного блока. напрмер:
a.tpl ---- {block name=head}parent{/block} ---- b.tpl ---- {extends template="a.tpl"} {block name="head"}child{/block} {/extends} {extends template="a.tpl"}{/extends} ---при отображении b.tpl будет выведено:
Следующий код позволяет решить эту проблему:
плагины:
<?php function smarty_block_block($params, $content, $smarty) { if ( !array_key_exists('name', $params) ) { $smarty->trigger_error('Block name is not set'); } $name = $params['name']; if ( !$smarty->isBlockSet($name) && !is_null($content) ) { $smarty->setBlock($name, $content); } if ( !is_null($content) ) { return $smarty->getBlock($name); } } ?> <?php function smarty_block_extends($params, $content, $smarty) { if ( !array_key_exists('template', $params) ) { $smarty->trigger_error('Plese set extending template name!'); } // if open tag if ( is_null($content) ) { $smarty->openBlocksScope(); } else { $content = $smarty->fetch($params['template']); $smarty->closeBlocksScope(); return $content; } return ''; } ?>поля и методы класса SmartyX (extends Smarty)
protected $_blocks = array(array()); protected $_blocksScope = 0; ... public function openBlocksScope() { $this->_blocksScope++; $this->_blocks[$this->_blocksScope] = array(); } public function closeBlocksScope() { if ( $this->_blocksScope > 0 ) { $this->_blocks[$this->_blocksScope] = array(); $this->_blocksScope--; } } public function isBlockSet($key) { return array_key_exists($key, $this->_blocks[$this->_blocksScope]) !== false; } public function setBlock($key, $value) { $this->_blocks[$this->_blocksScope][$key] = $value; } public function getBlock($key) { if (array_key_exists($key, $this->_blocks[$this->_blocksScope])) { return $this->_blocks[$this->_blocksScope][$key]; } return ''; }$this->register_outputfilter( array(&$this, 'gz_compress') );Проблема заключается в том что обвертываемый шаблон проходит дважды через фильтр вывода (первый раз при вызове функции
$smarty->fetch($params['template']);а второй уже при выводе шаблона display()).Решения проблемы пока не нашел :(