Pull to refresh

Comments 54

шикарно :) ещё один повод любить смарти!
Справедливости ради надо сказать, что подобная фишка сработает много где, даже на pure php её достаточно просто реализовать.

Но спасибо за положительный отзыв :)
я смарти люблю в первую очередь именно за гибкость, которая достигается плагинами… не так давно построил на них чуть ли ни целый фреймворк :)
Делитесь идеями, они важны =)
Делитесь идеями, многим полезно будет :)
Такими темпами функциональность smarty доведут до уровня самого php и таки напишут шаблонизатор на шаблонизаторе, написанном на языке, который сам в свою очередь изначально был шаблонизатором. Прямо дух захватывает от такой многоуровневости ;)
Подробнее-подробнее!
как уже правильно заметил сам автор, это совсем не крутизна Смарти. Это идея, которую можно применить где угодно. Идея хорошая, спасибо.
Мне в смарти доставляет удовольствие его способность кешировать. Объявил какой либо шаблон, что наполняется данными, закешировал и всё база не вызывается лишний раз. Правда в одном из моих проектов таким способом бывает накешируется до 50 — 60 мегабайт (2 000 — 3 000 файлов), не знаю при поиске шаблона из такого колличества файлов — что быстрее — запрос к базе или же перебор смарти при поиске закешированного шаблона.
Ну… тема кеширования в шаблонах — тема для отдельной статьи. И тема эта, надо сказать, противоречивая :)
хранить кэши блоков и страниц можно и не в файловой системе.

правда, обычно это делается не на уровне шаблонизатора.

например, для Drupal есть модуль cache router, который позволяет распределять кэшируемые объекты по разным хранилищам (файловая система, БД, memcached.
function smarty_block_block($params, $content, Smarty $smarty) — здесь имеется неточность: type-hinting нужно указывать как mySmarty $smarty — ведь в оригинальном Smarty нет методов set/getBlock.
Не думаю, что кто-то в пределах одного проекта будет использовать и Smarty и его расширенную версию. Однако, Вы правы — это баг. Исправлю.
Около года назад имел дело с наследованием в смарти и решение использ-овал несколько отличное от этого.
Вечером постараюсь дома написать топик на хабр со своим вариантом.
Будет очень интересно узнать :)
Не раз слышал критику Smarty, один из основных недостатков называли — отсутствие наследования шаблонов :)

Спасибо за решение.
За решание — пожалуйста :)

А насчёт критики — занятно. Все обычно акцентируют внимание на тормознутости и «интерпретируемый язык на интерпретируемом языке», про отсутствие наследования никто не вспоминал; я, если честно, вообще не знаю на PHP шаблнизаторов с реализованным уже наследованием. Они, похоже, не существуют или не получили широкого распространения.
Ну, честно говоря, никогда не сталкивался с их жесткой необходимостью. Это интересная фишка на заметку, но зачастую она вам не пригодится. Потому она и не получила широкого распространения ;)
Про «не пригодится» не соглашусь.

Жёсткой необходимости нет почти ни в чём. Можно ведь и не заморачиваться на ООП в целом и Смарти в частности, а просто писать код линейно и потоком, не заморачиваясь на такие мелочи, как разделение логик приложения и отображения :). Вопрос в том, насколько потом ЭТО будет легко поддерживаться.

То же самое и с шаблонами. Я уже на собственном опыте убедился, что подобный подход проще в дальнейшем расширении. Особенно актуально для создания новых шаблонов на основе уже существующих :)
ну в том же ZF вроде как можно из шаблона вызывать другие контроллеры (а не тот который отрабатывается сейчас). И основной контроллер совершенно не обязан о них думать. В шаблон добавляем $this->action() и понеслась
xslt. и не только на пхп…
Хоть убейте меня, но я не понимаю чем это отличается от include?
тем, что инклуд не меняется. В случае одного блока вам понадобится два инклуда код_до и код после. В случае двух блоков, уже три итд. В случае наследования, вам всегда нужен один (если наследуемся от одного конечно).
был бы благодарен примером :)
он есть в посте. Там два файла parent.tpl и child.tpl в случае инклуда вам бы понадобились хидер и футер, вместо парента. Добавьте еще блок и вам понадобится хидер, футер и миддлер (гыгы).
UFO landed and left these words here
Если честно, не совсем понял, где в Вашем случае наследование… Расскажите поподробнее, пожалуйста.

А насчёт «ЗЫ»: что такое за*loop*ленные включения шаблонов? Если это просто инклюд шаблона внутри {loop}{/loop} или {foreach}{/foreach}, то, как мне кажется, проблем возникнуть не должно. Если ошибся — поправьте. Желательно с примерами :)
UFO landed and left these words here
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}
if (in_array($value, $this->_blocks[$key]) === false) { array_push($this->_blocks[$key], $value); }

Вот этот момент, по-моему, лишний. Если я присвоил блоку значение, потом его переопределил, а потом хочу переопределить на предыдущее (хоть я этого, наверное, ни разу и не делал), то я хочу всё же получить последнее переопределенное значение. А так получается, что я могу написать:

<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), увидите, что я имею в виду.

Я пошёл на это ограничение, исходя именно из тех соображений, что случаи, когда «внук» будет точно таким же, как его «дедушка», довольно редки и ими можно пренебречь :)
А вообще — спасибо за статью, коротко и ясно. Мне этого, честно говоря, не хватало! :)
А чем хуже вынести содержимое блока, который нужно подменить во внешний файл (в несколько внешних файлов) и в зависимости от параметров эти файлы подключать? Получится композиция вместо наследования, что по сути есть более гибкое решение.
UFO landed and left these words here
Я отказался от смарти когда стал использовать больше ООП.
ведь конструкция типа
{$item->getColorById(1)->getSizeById(2)->name}
не работала :-(
Это элементарно исправляется.
подскажите?
знаю только что нужно регекспы патчить, но они там такие, что чёрт ногу сломит…
Да, нужно исправить одно регулярное выражение для разыменования объектов и методов и второе — для доступа к публичным свойствам класса. Если интересно, ближе к вечеру выложу
А мои бывшие коллеги до сих пор его используют. Изобретая чудовищные костыли для обхода вот таких вот недоработок.
Хотя ходят слухи, что где-то существует приватная версия смарти, которая позволяет вызывать методы объектов цепочкой… ;)
Хотите увидеть эту приватную версию? :))
Разве только в виде патча. И то, интерес чисто академический, т.к. Smarty не пользую уже давно. И, если всё будет хорошо, не буду и в дальнейшем.
извиняюсь, что вторгаюсь в беседу, но уж очень любопытно глянуть на исправленную версию Smarty…
нашел еще пару интересных особенностей этого решения.
Приведу реальный пример из своей практики.

У меня есть класс 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
array_push($this->_blocks[$key], $value);

на
array_unshift($this->_blocks[$key], $value);
$this->_blocks[$key][count($this->_blocks[$key])-1]
лучше заменить на
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 будет выведено:
child

child


Следующий код позволяет решить эту проблему:

плагины:
<?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 '';
    }


Данный подход вызывает проблемы при использовании фильтров вывода, например сжатия страницы перед вызовом функции display:
$this->register_outputfilter( array(&$this, 'gz_compress') );

Проблема заключается в том что обвертываемый шаблон проходит дважды через фильтр вывода (первый раз при вызове функции $smarty->fetch($params['template']); а второй уже при выводе шаблона display()).

Решения проблемы пока не нашел :(
Вариант решения проблемы с output фильтрами:

0. Добавляете в свой унаследованный класс smarty поле is_page
public $is_page = false;

1. создаёте smarty-функцию {eop}
/**
* End Of Page
* @return
* @param object $params
* @param object $smarty
*/
function smarty_function_eop($params, &$smarty)
{
$smarty->is_page = true;
}

2. В конце главного шаблона вызываете функцию:
</body>
</html>
{eop}

3. В самом начале output фильтра проверяете, а не конец ли страницы
function smarty_outputfilter_head($tpl_output, &$smarty)
{
if (!$smarty->is_page)
return $tpl_output;

}

4. Правите smarty_block_extends:
function smarty_block_extends($params, $content, $smarty, $repeat)
{
/** Никому не доверяйте. Даже себе! */
if (false === array_key_exists('template', $params)) {
$smarty->trigger_error('Укажите шаблон, от которого наследуетесь!');
}

if (!$repeat)
return $smarty->fetch('pages/'.$params['template']);
}
Вариант решения проблемы с output фильтрами:

0. Добавляете в свой унаследованный класс smarty поле is_page
public $is_page = false;

1. создаёте smarty-функцию {eop}
/**
* End Of Page
* @return
* @param object $params
* @param object $smarty
*/
function smarty_function_eop($params, &$smarty)
{
$smarty->is_page = true;
}

2. В конце главного шаблона вызываете функцию:


{eop}

3. В самом начале output фильтра проверяете, а не конец ли страницы
function smarty_outputfilter_head($tpl_output, &$smarty)
{
if (!$smarty->is_page)
return $tpl_output;

}

4. Правите smarty_block_extends:
function smarty_block_extends($params, $content, $smarty, $repeat)
{
/** Никому не доверяйте. Даже себе! */
if (false === array_key_exists('template', $params)) {
$smarty->trigger_error('Укажите шаблон, от которого наследуетесь!');
}

if (!$repeat)
return $smarty->fetch('pages/'.$params['template']);
}
Попробовал, понравилось.
Всё было хорошо пока не включил кэширование в Smarty.
Наследование тут же отвалилось, пробую понять что к чему, но изнутри Samarty знаю плохо.

Сталкивались с подобным?
Only those users with full accounts are able to leave comments. Log in, please.