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

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

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

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

Поэтому, в конце урока вам предлагается выбрать, как именно мы будем работать дальше. На всякий случай, вот как должен выглядеть наш сайт после этого урока — s1889.h3.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,
),
Всё, что у нас получилось, можно смотреть по ссылкам:
А вот все изменения на 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 человека, поэтому предлагаю вам самим решить, какие дальше будут уроки.

Следующая заметка
Осваиваем Composer
Предыдущая заметка
Подключаем шаблонизатор Fenom


Комментарии ()

  1. Семён Лобачевский 06 июня 2015, 13:41 # +1
    Мне было бы интересно узнать про третий вариант:
    — Устанавливаем фреймворк xPDO, пишем схему таблиц, генерируем модель и строим запросы с его помощью
    1. Василий Наумкин 06 июня 2015, 13:50 # 0
      Да, мне он тоже больше нравится.

      Если никто не будет против — то дальше работаем с ним.
      1. Перетягин Илья 06 июня 2015, 15:14 # 0
        Мы таким макаром придем от чистого пхп к движкам. Я в целом не против, но не и не поддерживаю.
        1. Василий Наумкин 06 июня 2015, 15:22 # 0
          Выходит именно так: или продолжать велосипедить, или использовать то, что уже придумали другие.

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

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

          Ты если хочешь, чтобы я еще про что-то отдельно написал — просто скажи, про что именно.
          1. Перетягин Илья 06 июня 2015, 15:33 # 0
            Я еще не могу добраться до второго урока, так как времени нету, по этому даже и не знаю, что нужно. Если ты говоришь, что больше писать нечего, значит так и есть.
            1. Василий Наумкин 06 июня 2015, 15:40 # 0
              Ясно. Ну, когда доберёшься, думаю, вопросов не будет.

              А если будут, то я тебе персонально всё расскажу =) Вопросы можно будет задавать еще месяца 3, а то и больше, после окончания курса.
              1. Перетягин Илья 06 июня 2015, 15:42 # 0
                Хорошо, спасибо большое!
      2. Максим Степанов 08 ноября 2015, 11:10 # 0
        Здравствуйте Василий. Подскажите а как расширяя базовый шаблон можно убрать sidebar и оставить только content?
        1. Василий Наумкин 08 ноября 2015, 11:18 # 0
          В моём примере его нельзя убрать, можно только оставить пустым.

          Но можно добавить еще один блок в основном шаблоне и заменять уже его:
          <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}
          1. Максим Степанов 08 ноября 2015, 11:24 # 0
            Понял, спасибо
        2. Николай Савин 04 января 2016, 09:37 # 0
          В начале урока в первом блоке кода класса 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?

          1. Василий Наумкин 04 января 2016, 09:44 # 0
            Да, абсолютно всё, что ты написал — верно.
          Добавление новых комментариев отключено.