Наследование шаблонов в PHP без использования сторонних библиотек
При разработке Web-приложений мы обязательно сталкиваемся с проблемами рендеринга HTML-страниц. Обычно эти проблемы решает шаблонизатор — собственно PHP или какой-нибудь парсер шаблонов. Если приложение большое и страницы содержат множество блоков, то сложность шаблонов может резко возрасти, а у разработчиков появляется желание упростить работу с ними. В ход идут разные техники, но обычно это выделение в шаблонах повторяющихся блоков и правильная их декомпозиция — включая наследование шаблонов.
Мне нравится, как сделано наследование шаблонов в Django. Идея простая — есть базовый шаблон, в нем выделены контентные блоки, которые будут меняться в зависимости от страницы. При рендеренге страницы можно указать, что за основу берется базовый шаблон и переопределить только нужные блоки. Если в проекте много однотипных страниц, то можно сделать промежуточный шаблон, наследующий от главного, а затем переопределять его данные. При умелом проектировании количество повторяющегося кода можно свести на нет, а также облегчить жизнь при изменении дизайна.
Похоже этот подход нравится не только мне и разработчикам Django, но и Fabien Potencier автору фреймворка Symfony и шаблонизатора Twig. Twig обладает множеством интересных функций, включая компиляцию шаблонов в нативные PHP-классы, фильтрацию данных, встроенными циклами и т.д. — в общем всем тем, что полагается иметь современному шаблонизатору. Но самое интересное — это то самое наследование шаблонов, о котором я говорил выше. Вот пример из официальной документации:
© Copyright 2011 by you.
Index > .importantIndex
Welcome on my awesome homepage.
В базовом шаблоне определены основные блоки, а в дочернем — пример их переопределения. Копания в исходных кодах показало, что реализуется это наследование также нативно — путем наследования “скомпилированных” классов. Отличная идея! Все хорошо, но меня несколько смущала необходимость изучения пусть простого, но все-таки отличного от PHP синтаксиса шаблонизатора. А моя нелюбовь к ненативным шабонам началась еще со Smarty (в котором тоже есть наследование) и до сегодняшнего дня не претерпела существенных изменений.
Совсем близко к иделу подошел разработчик из Сан-Франциско Adam Shaw. Очевидно, он также как и я не любит эксперименты с синтаксисом шаблонизаторов и придумал незамысловато названную библиотеку Template Inheritance На сайте жирным по желтому написано, что «There is no need to learn another template language», мол не нужно учить другой язык шаблонов. С этим я согласен. Что же он предлагает? Смотрим пример опять же из официальной документации:
This is the title This is the article
Синтаксис натуральный, блоки выделены явно, подключил библиотеку в базовый шаблон и забыл. Все. Автор говорит, что сделал это с помощью буферов и стека (возможны вложенные блоки). Код действительно интересный, но пестрит наличием глобальных переменных. Чего же остается ещё желать?
Вот здесь-то мы и подходим к главной теме нашего повествования. А не сможет ли PHP сам переопределить блоки базового шаблона? Я думаю, что вполне! Смотрите:
else < ?>Default content ?> else < ?>Default sidebar ?>
Здесь в 3 блоках шаблона проверяется наличие соответствующей переменной, хранящей некоторый контент и, если она присутствует в области видимости, то ее можно выводить в шаблон, а если нет, то выводится контент по-умолчанию. Нам остается только переопределить эти переменные в дочернем шаблоне. А вот собственно и он:
В этом примере переопределяется переменная $content, если она не была установлена заранее. Это сделано для того, чтобы была возможность наследовать этот шаблон и переопределить блок контента. Думаю, идея понятна. Не требуется никаких библиотек — просто пишите шаблоны в таком стиле и будет вам счастье.
Конечно, и здесь не обошлось без недостатков. Во-первых, это не очень лаконичный синтаксис определения и переопределения блоков: расплата за нативность. Во вторых, в дочернем шаблоне нельзя получить код родительского блока. В-третьих, при таком способе включения шаблонов перед собственно HTML-кодом может образоваться некоторое количество пробелов из-за отступов между блоками. Здесь я бы посоветовал подключать шаблон также с помощью буферов и фильтровать контент. Так делается во многих фреймворках:
function render($pathToTemplate, $data)
Эта функция возвращает вывод шаблона из файле $pathToTemplate с подстановкой переменных, полученных из массива $data. extract — на любителя — можно и не делать, а обращаться напрямую к $data. Перед выводом из контента убираются начальные и конечные пробелы. В шаблоне можно делать все, что позволит делать ваша совесть, не нарушая принципы разделения логики и представления, и PHP. Например, в зависимости от ситуации подключать тот или иной базовый файл.
Вот и все. Для реализации наследования можно использовать любой из описанных выше методов, уверен, что есть еще что-то. Буду рад, если эта статья поможет кому-то сделать свой код немного лучше.
Наследование расширение шаблонов
На этом занятии поговорим о механизме расширения шаблонов в Jninja. Его еще называют наследованием. Это достаточно удобный инструмент, уменьшающий объем дублируемого кода в шаблонах. Рассмотрим принцип его работы на нашем примере с HTML-страницами сайта.
Для простоты восприятия возьмем целостную страницу и представим ее в таком виде (файл ex_main.htm):
DOCTYPE html> html> head> meta charset="UTF-8"> title>{% block title %}{% endblock %}/title> /head> body> {% block content %} {% endblock %} /body> /html>
Смотрите, здесь используется новый тип блоков – именованные блоки, которые в самом простом случае записываются по синтаксису:
Эти блоки как раз и используются для создания расширения базового шаблона страницы ex_main.htm для создания всех страниц текущего сайта. Расширение (или наследование) шаблона делается следующим образом (файл about.htm):
{% extends 'ex_main.htm' %} {% block title%}О сайте{% endblock %} {% block content %} h1>О сайте/h1> p>Классный сайт, если его доделать./p> {% endblock %}
Первой строчкой мы указываем базовый шаблон ‘ex_main.htm’, который собираемся расширять в шаблоне about.htm. Затем, указываем, что первый именованный блок title будет содержать строку «О сайте», а второй (content) – текст, помещенный в него.
Далее, в самой программе на Python мы делаем следующее:
from jinja2 import Environment, FileSystemLoader file_loader = FileSystemLoader('templates') env = Environment(loader=file_loader) template = env.get_template('about.htm') output = template.render() print(output)
Используя файловый загрузчик, берем файл шаблона about.htm из подкаталога templates и, затем, обрабатываем его с помощью метода render. На выходе получим следующую HTML-страницу:
DOCTYPE html> html> head> meta charset="UTF-8"> title>О сайте/title> /head> body> h1>О сайте/h1> p>Классный сайт, если его доделать./p> /body> /html>
Как видите, благодаря использованию механизма наследования шаблонов, легко и просто можно формировать страницы самого разного уровня сложности.
Разумеется, если базовый шаблон находится в другом каталоге относительно дочернего about.htm, то в инструкции extends это явно нужно прописать, например, так:
Будет взят шаблон default.tpl из подкаталога layout, находящийся в каталоге templates.
Далее, если нам нужно один и тот же блок использовать несколько раз на странице, то это делается так:
{% extends 'layout/default.tpl' %} {% block title%}О сайте{% endblock %} {% block content %} h1>{{ self.title() }}/h1> p>Классный сайт, если его доделать./p> {% endblock %}
Здесь self ссылается на объект текущего шаблона, в котором имеется метод title, т.к. существует блок с таким именем. И, далее, вызывая его, будет напечатано содержимое этого блока. Так можно делать с любым именованным блоком.
Ну а раз есть параметр self, то должен быть и super, который обращается к блоку базового шаблона и берет информацию непосредственно из него. В качестве простой демонстрации добавим в блок content базового шаблона строку «Блок контента». А в дочернем шаблоне about.htm сделаем вызов:
{% block content %} {{ super() }} h1>{{ self.title() }}/h1> p>Классный сайт, если его доделать./p> {% endblock %}
В итоге, получим результат:
Блок контента
О сайте
Классный сайт, если его доделать.
Если же вызов super убрать, то останутся только две строчки:
О сайте
Классный сайт, если его доделать.
Этот пример также показывает, что содержимое именованных блоков полностью замещается содержимым, указанным в дочерних шаблонах. И если нам нужно добавить новую информацию в производном шаблоне к информации блока базового шаблона, то следует вызывать метод super.
Вложенные блоки
При необходимости блоки можно вкладывать друг в друга создавая их иерархию. Например, пропишем в шаблоне базового класса в блоке content еще один блок – table_contents:
{% block content %} {% block table_contents %} ul> {% for li in list_table -%} li>{{li}}/li> {% endfor -%} /ul> {% endblock table_contents %} {% endblock content %}
Обратите внимание, здесь после endblock дополнительно указано какой блок заканчивается. Это необязательная запись, но она помогает лучше ориентироваться в сложных шаблонах. Далее, в дочернем шаблоне about.htm обратиться к этому вложенному блоку можно так:
{% block content %} {{ super() }} h1>{{ self.title() }}/h1> p>Классный сайт, если его доделать./p> {% endblock %}
{% block content %} {% block table_contents %}{{ super() }}{% endblock %} h1>{{ self.title() }}/h1> p>Классный сайт, если его доделать./p> {% endblock %}
Вот этот второй вариант более гибкий, т.к. в block content базового шаблона могут присутствовать и другие именованные блоки и мы здесь добавляем только один — table_contents, остальные будут проигнорированы. Если же убрать строчку с вложенным блоком:
{% block content %} h1>{{ self.title() }}/h1> p>Классный сайт, если его доделать./p> {% endblock %}
То получим страницу без оглавления. Видите, это очень удобно: если нужно добавляем буквально одной строчкой, а если не надо, то ничего не пишем.
Область видимости блоков
Давайте теперь, немного усовершенствуем базовый шаблон и добавим еще один блок для формирования элементов списка:
{% for li in list_table -%} li>{% block item %}{{ li }}{% endblock %}/li> {% endfor -%}
Если теперь выполнить программу, то внутри тегов li не будет никакой информации. Дело в том, что внутри блока item доступ к внешней переменной li нет. Чтобы исправить эту ситуацию и разрешить оперировать переменными из внешней области видимости, после имени блока следует прописать ключевое слово scoped:
li>{% block item scoped %}{{ li }}{% endblock %}/li>
Теперь при запуске программа будет работать также, как и ранее. Но мы же добавили этот блок item не просто так, значит, собираемся его переопределять в дочернем шаблоне. И это можно сделать следующим образом:
{% block item %}p class="item">{{ super() }}/p>{% endblock %}
Смотрите, мы здесь воспользовались функцией super, чтобы получить текущее значение списка и дополнительно еще прописали тег p с соответствующим стилем оформления. При запуске увидим следующий результат:
Вот такая гибкость может быть достигнута за счет использования механизма наследования и именованных блоков.
Вложенное наследование шаблонов
В заключение этого занятия отмечу один, в общем-то, очевидный момент: шаблоны поддерживают вложенное наследование, то есть, формирование итогового дочернего шаблона по цепочке: от базового (корневого) до последнего – выходного. Например, вот такую иерархию: