Расширение и наследование шаблонов 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
  1. Тот расширяет шаблон _base
  2. При расширении заменяется блок content
  3. А внутри замены вызывается {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/

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

Мы разобрали механизм расширения, а теперь я предлагаю познакомиться с механизмом наследования (включения одного шаблона в другой).
Давайте добавим уже на сайт навигационную панель 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. Продолжаем работу на файлах, без БД
  1. Работаем с БД через PDO, "сырыми" SQL запросами
  2. Устанавливаем фреймворк xPDO, пишем схему таблиц, генерируем модель и строим запросы с его помощью
Пока что на курсах всего 3 человека, поэтому предлагаю вам самим решить, какие дальше будут уроки.

12 комментариев

Семён Лобачевский
Мне было бы интересно узнать про третий вариант: - Устанавливаем фреймворк xPDO, пишем схему таблиц, генерируем модель и строим запросы с его помощью
Василий Наумкин
Да, мне он тоже больше нравится.
Если никто не будет против - то дальше работаем с ним.
Перетягин Илья
Мы таким макаром придем от чистого пхп к движкам. Я в целом не против, но не и не поддерживаю.
Василий Наумкин
Выходит именно так: или продолжать велосипедить, или использовать то, что уже придумали другие.
В реальной работе ты же не будешь писать всё сам - зачем? На данный момент мы уже написали ядро сайта на чистом PHP, которое вполне понятно и прозрачно использует сторонний шаблонизатор.
Поэтому, давайте решать сейчас, как будем продолжать занятия. Я вот не знаю даже, что еще писать на чистом PHP. У меня все дороги ведут в Composer и загрузку готовых решений - это и проще и удобнее.
Ты если хочешь, чтобы я еще про что-то отдельно написал - просто скажи, про что именно.
Перетягин Илья
Я еще не могу добраться до второго урока, так как времени нету, по этому даже и не знаю, что нужно. Если ты говоришь, что больше писать нечего, значит так и есть.
Василий Наумкин
Ясно. Ну, когда доберёшься, думаю, вопросов не будет.
А если будут, то я тебе персонально всё расскажу =) Вопросы можно будет задавать еще месяца 3, а то и больше, после окончания курса.
Перетягин Илья
Хорошо, спасибо большое!
Максим Степанов
Здравствуйте Василий. Подскажите а как расширяя базовый шаблон можно убрать sidebar и оставить только content ?
Василий Наумкин
В моём примере его нельзя убрать, можно только оставить пустым.
Но можно добавить еще один блок в основном шаблоне и заменять уже его:

<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}
Максим Степанов
Понял, спасибо
Николай Савин
В начале урока в первом блоке кода класса 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.ru
Личный сайт Василия Наумкина
Прямой эфир
Александр Наумов
23.07.2024, 00:20:37
Василий, спасибо большое!!
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так. А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен. Спасибо, исправил!
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500 Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи. ...
Василий Наумкин
20.03.2024, 21:21:52
Volledig!
Андрей
14.03.2024, 13:47:10
Василий! Как всегда очень круто! Моё почтение!
russel gal
09.03.2024, 20:17:18
> А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал...
Александр Наумов
27.01.2024, 03:06:18
Василий, спасибо! Извини, тупанул.
Василий Наумкин
22.01.2024, 07:43:20
Давай-давай!
Василий Наумкин
24.12.2023, 14:26:13
Спасибо!