Расширение и наследование шаблонов Fenom

На прошлом занятии мы подключили шаблонизатор Fenom к нашей системе и написали простенький шаблон.

Теперь пришло время написать уже нормальные шаблоны для страниц Home и Test, при этом они будут наследовать один общий шаблон Base, в котором будет генерироваться меню сайта.

В принципе, основная часть сайта после этого будет закончена и нужно будет только наращивать функционал - писать тексты, выводить их в шаблонах с разбивкой на страницы и прочая, привычная по MODX работа.

Поэтому, в конце урока вам предлагается выбрать, как именно мы будем работать дальше. На всякий случай, вот как должен выглядеть наш сайт после этого урока - http://s1889.bez.modhost.pro.

Рендер шаблонов

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

Пускай будет один метод template в Controller, а при необходимости дочерние контроллеры всё равно смогут его расширить:


    /**
     * Шаблонизация
     *
     * @param string $tpl Имя шаблона
     * @param array $data Массив данных для подстановки
     * @param Controller|null $controller Контроллер для передачи в шаблон
     *
     * @return mixed|string
     */
    public function template($tpl, array $data = array(), $controller = null) {
        $output = '';
        if (!preg_match('#\.tpl$#', $tpl)) {
            $tpl .= '.tpl';
        }
        if ($fenom = $this->core->getFenom()) {
            try {
                $data['_core'] = $this->core;
                $data['_controller'] = !empty($controller) && $controller instanceof Controller
                    ? $controller
                    : $this;
                $output = $fenom->fetch($tpl, $data);
            }
            catch (Exception $e) {
                $this->core->log($e->getMessage());
            }
        }

        return $output;
    }

Думаю, по предыдущим урокам уже понятно, что здесь происходит: мы пытаемся оформить данные указанным шаблоном, а если этот процесс выбросит какое-то исключение, то мы запишем его в лог и вернём пустоту.

Нужно обратить внимание, что в этом методы мы передаём в массив данных наш объект Сore в переменную {$_core} - таким образом, шаблон сможет выполнять любые методы основного класса и получать системные настройки. Для тех же целей рядышком и объект {$_controller}, который мы можем передать из дочернего контроллера. А если не передали - то там будет базовый контроллер.

Таким образом, в любом шаблоне мы сразу получаем ссылку на ядро и контроллер со всеми их публичными методами и свойствами. Вот зачем мы изначально определяли эти public, protected и private - чтобы шаблон не мог использовать всё подряд.

Теперь метод run() нашего дочернего контроллера Home выглядит вот так:


    public function run() {
        return $this->template('home', array(
            'pagetitle' => 'Тестовый сайт',
            'longtitle' => 'Третий курс обучения',
            'content' => 'Текст главной страницы курса обучения на bezumkin.ru',
        ), $this);
    }

Как видите, последним параметром мы передаём ссылку на сам контроллер Home. Выгружаем наши изменения на GitHub для истории.

Расширение шаблонов

Ну вот и пришло время нам написать базовый шаблон и дочерние, которые будут его расширять. Логика очень похожа на PHP, только с поправкой на синтаксис Fenom.

Те шаблоны, которые мы не будем вызывать напрямую, а будем только использовать в других шаблонах, сразу предлагаю называть с подчёркивания. Вот наш шаблон /core/Templates/_base.tpl:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>
        {block 'title'}Тестовые уроки на bezumkin.ru{/block}
    </title>
    {block 'css'}
        <link rel="stylesheet" href="/assets/css/bootstrap.min.css">
    {/block}
</head>
<body>
    <div class="container">
        <div class="row">
            <div class="col-md-10">
                {block 'content'}
                    {if $longtitle != ''}
                        <h3>{$longtitle}</h3>
                    {elseif $pagetitle != ''}
                        <h3>{$pagetitle}</h3>
                    {/if}
                    {$content}
                {/block}
            </div>
            <div class="col-md-2">
                {block 'sidebar'}
                    Сайдбар
                {/block}
            </div>
        </div>
    </div>
</body>
<footer>
    {block 'js'}
        <script src="/assets/js/jquery-2.1.4.min.js"></script>
        <script src="/assets/js/bootstrap.min.js"></script>
    {/block}
</footer>
</html>

Все части шаблона, которые можно будет переопределить в дочерних, мы оборачиваем в {block 'имя'}. Так получаются блоки: - title - заголовок страницы

  • content - содержимое
  • sidebar - сайдбар справа, пока пустой
  • css - подключаемые стили css
  • js - подключаемые скрипты

В этих блоках у нас прописаны теги по-умолчанию, и теперь мы можем расширить их из дочерних шаблонов. Для этого в шаблоне home пишем следующее:


{extends '_base.tpl'}

{block 'content'}
    <div class="jumbotron">
        {parent}
    </div>
{/block}

Первая строка указывает на шаблон, который мы расширяем. Дальше мы переопределяем блок content, помещая всё стандартное содержимое в div с классом jumbotron. Специальный тег {parent} указывает, что мы используем именно содержимое родительского блока, а в нём у нас прописана вставка {$pagetitle} и {$content}.

Повторю логику еще раз: 1. Контроллер Home использует шаблон home 2. Тот расширяет шаблон _base 3. При расширении заменяется блок content 4. А внутри замены вызывается {parent}, что вставляет содержимое по-умолчанию в расширяющий блок

В итоге, во время компиляции, в шаблоне home выходит примерно вот так:


{block 'content'}
    <div class="jumbotron">
        {if $longtitle != ''}
            <h3>{$longtitle}</h3>
        {elseif $pagetitle != ''}
            <h3>{$pagetitle}</h3>
        {/if}
        {$content}
    </div>
{/block}

Мы расширили шаблон _base, используя значение родителя по-умолчанию.

А вот шаблон test пускай просто заменит тег title, а всё остальное оставит как есть:

{extends '_base.tpl'}

{block 'title'}
    {$title} / {parent}
{/block}

Здесь мы добавляем свой заголовок страницы к стандартному, через косую.

Чтобы шаблоны не ругались на отсутствующие переменные (например, на longtitle) и не писали на страницу E_NOTICE, я еще указал в настройках класса Core параметр force_verify для Fenom:

'fenomOptions' => array(
    'auto_reload' => true,
    'force_verify' => true,
),

Всё, что у нас получилось, можно смотреть по ссылкам: - http://s1889.bez.modhost.pro/

А вот все изменения на GitHub.

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

Мы разобрали механизм расширения, а теперь я предлагаю познакомиться с механизмом наследования (включения одного шаблона в другой).

Давайте добавим уже на сайт навигационную панель Bootstrap с пунктами меню. Пишем чанк /core/Templates/_navbar.tpl:


<nav class="navbar navbar-default">
    <div class="navbar-header">
        <a class="navbar-brand" href="/">Course 3</a>
    </div>
    <ul class="nav navbar-nav">
        {set $pages = $_controller->getMenu()}
        {foreach $pages as $name => $page}
            {if $_controller->name == $name}
                <li class="active">
                    <a href="#" style="cursor: default;" onclick="return false;">{$page.title}</a>
                </li>
            {else}
                <li><a href="{$page.link}">{$page.title}</a></li>
            {/if}
        {/foreach}
    </ul>
</nav>

Понятное дело, здесь нас интересует код генерации пунктов меню:


    {set $pages = $_controller->getMenu()}

    {foreach $pages as $name => $page}
        {if $_controller->name == $name}
            <li class="active">
                <a href="#" style="cursor: default;" onclick="return false;">{$page.title}</a>
            </li>
        {else}
            <li><a href="{$page.link}">{$page.title}</a></li>
        {/if}
    {/foreach}

Первым делом мы вызываем из контроллера метод getMenu(), вот он:


    /**
     * Возвращает пункты меню сайта
     *
     * @return array
     */
    public function getMenu() {
        return array(
            'home' => array(
                'title' => 'Главная',
                'link' => '/',
            ),
            'test' => array(
                'title' => 'Тестовая',
                'link' => '/test/',
            )
        );
    }

Особо не умничая, пока что, просто перечисляем наши страницы.

Дальше в шаблоне идёт прокрутка массива и определение текущей страницы. Для этого я добавил в контроллеры публичное свойство name - чтобы отличать их друг от друга.

Проверяя {$_controller->name} всегда можно понять, на какой страницы мы находимся и отметить этот пункт в панели активным.

Осталось только подключить вывод navbar в шаблоне _base:


    {block 'navbar'}
        {include '_navbar.tpl'}
    {/block}

Оборачивание в блок navbar позволяет нам, если что, переопределить навигационную панель из дочернего шаблона.

Заключение

Итоговый коммит с результатами сегодняшней работы - вот здесь. Как видите, основной движок сайта уже написан.

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

Теперь нужно определиться, в какую сторону мы дальше пойдём: 1. Продолжаем работу на файлах, без БД 2. Работаем с БД через PDO, "сырыми" SQL запросами 3. Устанавливаем фреймворк xPDO, пишем схему таблиц, генерируем модель и строим запросы с его помощью

Пока что на курсах всего 3 человека, поэтому предлагаю вам самим решить, какие дальше будут уроки.

← Предыдущая заметка
Подключаем шаблонизатор Fenom
Следующая заметка →
Осваиваем Composer
Комментарии (12)
Семён Лобачевский
06.06.2015 10:41

Мне было бы интересно узнать про третий вариант: - Устанавливаем фреймворк xPDO, пишем схему таблиц, генерируем модель и строим запросы с его помощью

bezumkinВасилий Наумкин
06.06.2015 10:50

Да, мне он тоже больше нравится.

Если никто не будет против - то дальше работаем с ним.

OnFoxПеретягин Илья
06.06.2015 12:14

Мы таким макаром придем от чистого пхп к движкам. Я в целом не против, но не и не поддерживаю.

bezumkinВасилий Наумкин
06.06.2015 12:22

Выходит именно так: или продолжать велосипедить, или использовать то, что уже придумали другие.

В реальной работе ты же не будешь писать всё сам - зачем? На данный момент мы уже написали ядро сайта на чистом PHP, которое вполне понятно и прозрачно использует сторонний шаблонизатор.

Поэтому, давайте решать сейчас, как будем продолжать занятия. Я вот не знаю даже, что еще писать на чистом PHP. У меня все дороги ведут в Composer и загрузку готовых решений - это и проще и удобнее.

Ты если хочешь, чтобы я еще про что-то отдельно написал - просто скажи, про что именно.

OnFoxПеретягин Илья
06.06.2015 12:33

Я еще не могу добраться до второго урока, так как времени нету, по этому даже и не знаю, что нужно. Если ты говоришь, что больше писать нечего, значит так и есть.

bezumkinВасилий Наумкин
06.06.2015 12:40

Ясно. Ну, когда доберёшься, думаю, вопросов не будет.

А если будут, то я тебе персонально всё расскажу =) Вопросы можно будет задавать еще месяца 3, а то и больше, после окончания курса.

OnFoxПеретягин Илья
06.06.2015 12:42

Хорошо, спасибо большое!

Максим Степанов
08.11.2015 08:10

Здравствуйте Василий. Подскажите а как расширяя базовый шаблон можно убрать sidebar и оставить только content ?

bezumkinВасилий Наумкин
08.11.2015 08:18

В моём примере его нельзя убрать, можно только оставить пустым.

Но можно добавить еще один блок в основном шаблоне и заменять уже его:


<div class="container">
    {block 'container'}
    <div class="row">
        <div class="col-md-10">
            {block 'content'}
                {if $longtitle != ''}
                    <h3>{$longtitle}</h3>
                {elseif $pagetitle != ''}
                    <h3>{$pagetitle}</h3>
                {/if}
                {$content}
            {/block}
        </div>
        <div class="col-md-2">
            {block 'sidebar'}
                Сайдбар
            {/block}
        </div>
    </div>
    {/block}
</div>

Переопределяем блок container

{extends '_base.tpl'}

{block 'container'}
    <div class="col-md-12">
        Контент без сайдбара
    </div>
{/block}
Максим Степанов
08.11.2015 08:24

Понял, спасибо

biz87Николай Савин
04.01.2016 06:37

В начале урока в первом блоке кода класса Controller появилось несколько вопросов. Что за конструкция try catch - ладно загуглил, что такое instanceof тоже суть разобрал и понял. Остался только чисто теоретический вопрос по логике работы instanceof


$data['_controller'] = !empty($controller) && $controller instanceof Controller
                    ? $controller
                    : $this;

Данный оператор (instanceof) проверяет, является ли переданный объект $controler экземпляром класса Controller. Верно? Вроде так. Но мы передаем сюда объект Controllers_Home, к примеру. И выражение все равно остается верным, instanceof возвращает TRUE (или что там возвращается?). То есть Controllers_Home является экземпляром класса Controller? Это результат того, что класс Controllers_Home расширяет класс Controller?

bezumkinВасилий Наумкин
04.01.2016 06:44

Да, абсолютно всё, что ты написал - верно.

bezumkin
Василий Наумкин
04.07.2022 23:34
Что-то странное у тебя произошло: миграция есть, и вроде как выполнена, но таблицы при этом отсутств...
inetlover
Александр Наумов
03.07.2022 20:36
Василий, спасибо! Все понятно!
bezumkin
Василий Наумкин
02.07.2022 20:28
Спасибо, поправил!
bezumkin
Василий Наумкин
30.06.2022 03:58
Есть ли возможность формировать &quot;friendly URL aliases&quot;, используя аналог translit MODx? ...
bezumkin
Василий Наумкин
27.06.2022 03:32
Спасибо за исправления, очень выручаешь =) Но учитывая количество не описаных в заметке дополнительн...
bezumkin
Василий Наумкин
27.06.2022 03:10
что будет использоваться для вывода многоуровневого меню Посмотри как работают комментарии на этом ...
bezumkin
Василий Наумкин
25.06.2022 11:56
Поправил, спасибо!
bezumkin
Василий Наумкин
21.06.2022 01:58
onLoad(data) { this.total = data.total }, и onLoad({total}) { this.total = total }, В нашем случ...
bezumkin
Василий Наумкин
20.06.2022 14:01
Прекрасно тебя понимаю, я когда сам в этом разбирался - голова дымилась. Но зато теперь прямо-таки п...
bezumkin
Василий Наумкин
20.06.2022 09:30
Не надо, оно по умолчанию так - я просто чуть более подробно написал.