войти зарегистрироваться

PHP whois

индекс
169,28

Наследование шаблонов в Smarty

Когда-то, давным-давно, мне пришлось использовать небезызвестный шаблонизатор Smarty. Сначала я, понятное дело, возмущался и кричал, какая же гадость эта заливная рыба Smarty, а потом «распробовал» и втянулся. Те удобства, которые он давал, с лихвой компенсировали мысли о том, что есть и более быстрые шаблонные движки.

Шаблоны я обычно строил с помощью инклюдов: в начале подключался header.tpl, в конце — footer.tpl, в середине ещё что-нибудь нужное. В целом разметка получалась довольно аккуратной, но не проходило ощущение, что не хватает чего-то важного. Окончательно понимание этого чего-то появилось, когда мне случилось написать простенькое приложение на Django. И это «что-то», как все поняли, оказалось наследованием шаблонов. Простая, как и всё гениальное, идея позволяла существенно упростить шаблоны и избавиться от дублирующих блоков.



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

Как я уже писал выше, для реализации нам понадобится зарегистрировать 2 блока с именами extends и block, а так же ввести хранилище значений.

Пусть блок {extends}{/extends} будет отвечать за получение исходного кода шаблона-родителя, а {block}{/block} — за создание и переопределение наследуемых блоков.

Мануал поможет нам создать блоковые плагины:
block.extends.php:
<?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, введя массив стека и методы:

mySmarty.class.php
<?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 и пользоваться прелестями наследования шаблонов.

Ленивые могут скачать готовый пример шаблонов и пощупать на деле (архив весит 2.2 кб, Smarty в комплект поставки, естественно, не входит).

Спасибо за внимание :)

комментарии (51)

  • шикарно :) ещё один повод любить смарти!
    • Справедливости ради надо сказать, что подобная фишка сработает много где, даже на 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 в случае инклуда вам бы понадобились хидер и футер, вместо парента. Добавьте еще блок и вам понадобится хидер, футер и миддлер (гыгы).
          • Спасибо.
  • Похожая штука у меня называется «контейнерами» и обычно выглядит так:

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

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

    Решения проблемы пока не нашел :(
Только авторизованные пользователи могут оставлять комментарии. Авторизуйтесь, пожалуйста.