ФорумПрограммированиеPHP для идиотов → Моё наследование в шаблонах

Моё наследование в шаблонах

  • artoodetoo

    Сообщения: 5139 Репутация: N Группа: в ухо

    Spritz 20 апреля 2010 г. 4:00

    Вот честно, считаю, что наследование в шаблонах (а-ля django) это беспонтовая трата мозга. Особенно как это сделано в Twig — из шаблонов генерятся буквально классы PHP с невнятными именами — куча кода и очевидные тормоза.

    Мы пойдём другим путём. Я решился попробовать реализовать эту хню когда увидел в Symfony 2 такой фрагмент:
    [tt]<?php $view->extend('HelloBundle::layout') ?>[/tt]
    Типа шаблоны нативные, но за счет класса-обертки появляется дополнительный функционал.
    Неплохо. Ну давай клепать своё, чтобы было просто и красиво!

    v1:
    Заведем простейшую обертку над нативным шаблоном:
    [tt]index.php[/tt]

    <?php

    class MyView
    {
       public function render($id, $data)
       {
           extract($data);
           include 'templates/' . $id . '.php';
       }
    }

    // —– Test

    $view = new MyView();

    $view->render('test', array('title' => 'Blablabla', 'text' => 'The wind has blown over ther ocean'));

    [tt]templates/test.php[/tt]

    <h1><?php echo $title ?></h1>
    <p><?php echo $text ?></p>


    Негусто, но работает. Попытаемся в лоб обернуть этот шаблон в шапку и валенки.



    v2:
    Класс оставляем таким же, а в шаблоне инклудим постоянные составляющие.
    [tt]templates/test.php[/tt]

    <?php
    include 'templates/header.php';
    ?>
    <h1><?php echo $title ?></h1>
    <p><?php echo $text ?></p>
    <?php
    include 'templates/footer.php';
    ?>

    [tt]templates/header.php[/tt]

    <html>
    <head>
       <title><?php echo $title ?></title>
    </head>
    <body>
    <div id="content">

    [tt]templates/footer.php[/tt]

    </div>
    </body>
    </html>

    Работает, но это как-то некашерно.



    v3:
    Рожаем наследование шаблона через вызов метода extend() — позаимствуем слово из Symfony
    [tt]templates/test.php[/tt]

    <?php
    $this->extend('default');
    ?>
    <h1><?php echo $title ?></h1>
    <p><?php echo $text ?></p>

    [tt]templates/default.php[/tt]

    <?php
    $this->extend('page-layout');
    ?>
    <!– Default content –>
    <p>No content yet</p>

    [tt]templates/page-layout.php[/tt]

    <html>
    <head>
     <title><?php echo $title ?></title>
     <style>
       #sidebar { float: left; padding: 0 0.5em; width: 200px; border-right: solid 1px #b00; }
       #content { float: left; padding: 0 0.5em; margin-left: -1px;  border-left: solid 1px #b00; }
     </style>
    </head>
    <body>
    <div id="sidebar">
    <h4>Sidebar</h4>
    <?php $this->output('sidebar') ?>
    </div>
    <div id="content">
    <?php $this->output('content') ?>
    </div>
    </body>
    </html>

    Я тут сделал даже заготовку под сайдбар, но пока наследую/перекрываю только контент. Если в test.php удалить весь контент, то сработает контент из базового шаблона. В конечном счете весь контент в самом "родительском" шаблоне оборачивается в шапку и валенки. Красота!
    А вот и новый класс:
    [tt]index.php[/tt]

    <?php

    error_reporting(-1);

    class MyView
    {
       private
           $_parents = array(),
           $_blocks  = array();

       public function render($id, $data)
       {
           extract($data);
           include 'templates/' . $id . '.php';
           while (!empty($this->_parents)) {
               $this->_grab('content');
               include 'templates/' . array_pop($this->_parents) . '.php';
           }
       }

       private function extend($id)
       {
           array_push($this->_parents, $id);
           ob_start();
       }

       private function output($block, $default = '')
       {
           if (isset($this->_blocks[$block])) {
               echo $this->_blocks[$block];
           }
       }

       private function _grab($block)
       {
           if (isset($this->_blocks[$block])) {
               ob_end_clean();
               return;
           }
           $tmp = ob_get_clean();
           if (strlen(trim($tmp)) > 0) {
               $this->_blocks[$block] = $tmp;
           }
       }
    }

    // —– Test

    $view = new MyView();

    $view->render('test', array('title' => 'Blablabla', 'text' => 'The wind has blown over ther ocean'));

    Почти совершенство. Теперь надо как-то задавать другие блоки кроме основного контента.



    v4:
    Определение блоков через вызов blockBegin-blockEnd
    [tt]index.php[/tt]

    <?php

    error_reporting(-1);

    class MyView
    {
       private
           $_parents = array(),
           $_names   = array(),
           $_blocks  = array();

       public function render($id, $data)
       {
           extract($data);
           include 'templates/' . $id . '.php';
           while (!empty($this->_parents)) {
               $this->_grab('content');
               include 'templates/' . array_pop($this->_parents) . '.php';
           }
       }

       private function extend($id)
       {
           array_push($this->_parents, $id);
           ob_start();
       }

    private function blockBegin($block)
    {
    array_push($this->_names, $block);
    ob_start();
    }

    private function blockEnd()
    {
    $this->_grab(array_pop($this->_names));
    }

    private function output($block, $default = '')
    {
    echo (isset($this->_blocks[$block])) ?
    $this->_blocks[$block] :
    $default;
    }

    private function _grab($block)
    {
    if (isset($this->_blocks[$block])) {
    ob_end_clean();
    return;
    }
    $tmp = ob_get_clean();
    if (strlen(trim($tmp)) > 0) {
    $this->_blocks[$block] = $tmp;
    }
    }
    }

    // —– Test

    $view = new MyView();

    $view->render('test', array('title' => 'Blablabla', 'text' => 'The wind has blown over ther ocean'));

    [tt]templates/test.php[/tt]

    <?php
    $this->extend('default');
    ?>
    <h1><?php echo $title ?></h1>
    <p><?php echo $text ?></p>
    <?php $this->blockBegin('sidebar') ?>
    <!– New sidebar –>
    <ul>
    <li>lorem</li>
    <li>ipsum</li>
    <li>dolor</li>
    <li>sit</li>
    <li>amet</li>
    </ul>
    <?php $this->blockEnd() ?>

    Задача выполнена: просто и достаточно красиво, imho.

    ιιlllιlllι унц-унц
  • artoodetoo

    Сообщения: 5139 Репутация: N Группа: в ухо

    Spritz 20 апреля 2010 г. 4:22, спустя 21 минуту 47 секунд

    А вот ещё чисто для эстетов: извлекаем блоки через геттер.

    Вместо [tt]<?php $this->output('sidebar') ?>[/tt]
    Будем писать [tt]<?php echo $this->sidebar ?>[/tt]

    Пример во вложении
    ιιlllιlllι унц-унц
  • kostyl

    Сообщения: 5203 Репутация: N Группа: Джедаи

    Spritz 20 апреля 2010 г. 12:46, спустя 8 часов 24 минуты 20 секунд

    Прикольно, но я так понял, что в шаблоне… чёта я не очень понял, но чувствую какую-то штуку, которая что то не позволит сделать нормально…
  • phpdude

    Сообщения: 26624 Репутация: N Группа: в ухо

    Spritz 20 апреля 2010 г. 13:12, спустя 25 минут 45 секунд

    да, забавная технология - наследование щаблонов. я в свое время понял эту пробелму и обошелся плагинм к смарти))

    {content}
    {/content}

    который центровку вставлял в шаблоне в основном, но потом понял что блоками тоже надо чтото делать %)

    в общем то идея не нова и наш уважаемый артуудетоо просто показал в 30 строках всю очевидность таких страшных слов как "наследование шаблонов"
    Сапожник без сапог
  • kostyl

    Сообщения: 5203 Репутация: N Группа: Джедаи

    Spritz 20 апреля 2010 г. 13:17, спустя 5 минут 6 секунд

    да, это просто "наследование шаблонов" в "шаблонном" смысле этих слов ;)
  • artoodetoo

    Сообщения: 5139 Репутация: N Группа: в ухо

    Spritz 25 апреля 2010 г. 1:12, спустя 4 дня 11 часов 54 минуты

    Стоит связаться с какой-то задачей, потом фиг перестанешь о ней думать :)

    Что мне не нравится в шаблонизаторе выше — так это непоследовательность. Часть данных в локальных переменных (созданы через extract), а вот содержимое блоков добывается из свойств класса. Можно переделать всё единообразно:

       public function render($id, $data)
       {
           extract($data);
           include 'templates/' . $id . '.php';
           while (!empty($this->_parents)) {
               $this->_grab('content');
               extract($this->_blocks);
               include 'templates/' . array_pop($this->_parents) . '.php';
           }
       }

    Теперь весь вывод в шаблонах делается одинаково:

    <?php echo $title ?>
    Blablabla
    <?php echo $sidebar ?>


    Следующее, что можно улучшить — добавить свой "язык шаблонов", по вкусу. Мой класс будет платформой выполнения, а синтаксический разбор это дело класса-наследника. В моем классе оставляем точку вызова компилятора.

    Девочки и мальчики, дальше пойдет урок ООП.

       public function render($id, $data)
       {
           extract($data);
           $this->_prepare($id);
           include 'templates/' . $id . '.php';
           while (!empty($this->_parents)) {
               $this->_grab('content');
               extract($this->_blocks);
               $this->_prepare($id = array_pop($this->_parents));
               include 'templates/' . $id . '.php';
           }
       }

       protected function _prepare($id)
       {
       }

    Метод _prepare объявлен как protected — это приглашение к перекрытию его в наследниках.
    На этом с ЭТИМ классом всё!

    Как может выглядеть компилирующий класс:

    <?php

    require 'view.php';

    class CompileView extends View
    {
    private
    $_rules = array(
    '#^(\$.*)$#' => 'echo $1; ',
    '#^@(\$.*)$#' => 'echo htmlspecialchars($1, ENT_QUOTES); ',

    '#^extend\s+(.*)$#' => '$this->_extend($1); ',

    '#^block\s+(.*)$#' => '$this->_blockBegin($1); ',
    '#^/block$#' => '$this->_blockEnd(); ',

    '#^if\s+(.*)$#' => 'if (!empty($1)): ',
    '#^elseif\s+(.*)$#' => 'elseif (!empty($1)): ',
    '#^else$#' => 'else: ',
    '#^/if$#' => 'endif; ',

    '#^for\s+([^,]+)\s+in\s+(.*)$#' => 'foreach ($2 as $1): ',
    '#^for\s+([^,]+),([^,]+)\s+in\s+(.*)$#' => 'foreach ($3 as $1 => $2): ',
    '#^/for$#' => 'endforeach; ',
    );

    protected function _prepare($id)
    {
    $template = 'templates/' . $id . '.tpl.html';
    $compiled = 'templates/' . $id . '.php';

    if (!file_exists($template)) {
    throw new Exception('Template "' . $template . '" not found');
    }
    $timeT = filemtime($template);
    $timeC = file_exists($compiled) ? filemtime($compiled) : 0;

    // Compiled version is absent or obsolete
    if ($timeC < $timeT)
    {
    // Get template and find parts looks like "{macro}"
    $text = str_replace(array('<?', '?>'), array('<?', '?>'), file_get_contents($template));
    $parts = preg_split('/(\{.*\})/U', $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);

    $text = '';
    $wasPhp = FALSE;
    foreach ($parts as $part) {
    $isPhp = FALSE;
    if ($part{0} == '{' && $part{strlen($part) - 1} == '}') {
    $macro = substr($part, 1, -1);
    foreach ($this->_rules as $pattern => $replace) {
    if (preg_match($pattern, $macro)) {
    $part = preg_replace($pattern, $replace, $macro);
    $isPhp = TRUE;
    break;
    }
    }
    }
    if ($isPhp != $wasPhp) {
    if ($wasPhp) $text .= '?>';
    if ($isPhp)  $text .= '<?php ';
    }
    $wasPhp = $isPhp;
    $text .= $part;
    }

    file_put_contents($compiled, $text);
    }
    }
    }


    Очевидно, что синтаксис языка шаблонов можно очень легко изменять или дополнять через новые регулярки в $_rules.

    Во вложении новая версия.
    + плюс поместил в SVN
    Спустя 280 сек.
    [tt]templates/test.tpl.html[/tt] — это ИСХОДНЫЙ шаблон, который будет скомпилирован в test.php

    {extend 'default'}
    {if $title}<h1>{@$title}</h1>{/if}

    {if $rows}
    {for $s in $rows}
    {@$s}<br />
    {/for}
    {/if}

    {block 'sidebar'}
    <!– New sidebar –>
    <ul>
    <li>lorem</li>
    <li>ipsum</li>
    <li>dolor</li>
    <li>sit</li>
    <li>amet</li>
    </ul>
    {/block}


    Спустя 263 сек.
    P.S. Ебанутый здесь парсер bbcode.

    Вместо
    str_replace(array('<?', '?>'), array('<?', '?>')
    на самом деле было
    str_replace(array('<?', '?>'), array('&lt;?', '?&gt;'),
    ιιlllιlllι унц-унц

Пожалуйста, авторизуйтесь, чтобы написать комментарий!