Skip to content

Latest commit

 

History

History
executable file
·
410 lines (304 loc) · 30.7 KB

templates.md

File metadata and controls

executable file
·
410 lines (304 loc) · 30.7 KB

Шаблоны в PHP

Это перепечатка с исправлениями статьи, которая была опубликована на несуществующем более сайте http://www.phpinfo.su/articles/practice/shablony_v_php.html (оригинал в веб-архиве), автор: phpinfo.su

В конце добавлен маленький обзор современных шаблонизаторов.

Истоки шаблонизации

Давным-давно, после появления PHP, один умный человек сказал, что для того, чтобы программа на PHP оставалась легко модифицируемой и расширяемой, нужно отделять код скрипта от кода шаблона. Эту мудрую мысль подхватило сообщество php-программистов и началось: были написаны десятки книг, руководств и статей, авторы которых делились соображениями насчет того, как можно более-менее грамотно отделить PHP от HTML. Так появился известный шаблонизатор Smarty, так появились и другие шаблонные решения. Заметьте, я не зря выделил жирным шрифтом две фразы — не смотря на схожесть, они несут разный смысл.

Логика приложения и логика отображения

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

<?php
$result = $a + $b;

Эта незамысловатая операция по праву может называться бизнес-логикой или логикой приложения. Иначе говоря — это суть программы. Ничего больше от программы не требуется, кроме как вычислить сумму двух слагаемых. В конечном итоге данная программа (при условии, что значения переменных $a и $b определены) может получить два различных типа значения — либо ноль, либо отрицательное или положительное число.

Поскольку программа используется в web, то логично было бы отдавать результат её выполнения в виде HTML. При этом хотелось бы применить некоторую логику при выводе HTML-кода — если результат не равен нулю — вывести результат синеньким текстом, иначе вывести сообщение красным цветом, что мол извините, "бублик", тобишь ноль.

Новичок

Рассмотрим решение данной задачи новичком. Новичок ничего не слышал об отделении php-кода скрипта от html-кода шаблона и наверняка напишет программу примерно так:

<?php
echo "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">";
echo "<html>";
echo "<head>";
echo "   <title>Основной шаблон HTML-страницы</title>";
echo "</head>";
echo "<body>";

$a = isset($_GET['a']) && is_numeric($_GET['a']) ? $_GET['a'] : 0;
$b = isset($_GET['b']) && is_numeric($_GET['b']) ? $_GET['b'] : 0;

$result = $a + $b;

if ($result) {
    echo "<span style=\"color:blue; font-weight:bold\">Результат: $result</span>";
} else {
    echo "<span style=\"color:red; font-weight:bold\">Результат равен нулю!</span>";
}

echo "</body>";
echo "</html>";

Какие ошибки совершил новичок? Он смешал PHP код (логику приложения) и логику отображения. Что такое логика отображения? Это условие в управляющей конструкции if-else, выводящее HTML-код в зависимости от полученного результата. Логика отображения никак не связана с логикой приложения, она даже не знает, как был получен $result — через сложение двух переменных или через сложные математические алгоритмы, сопровождающиеся выборками из базы и запросами к стороннему серверу. Ей это всё равно, у логики отображения другая задача — показать пользователю результат работы программы.

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

Теперь представим, что новичок написал целый интернет-магазин в подобном стиле, смешав воедино выборки из базы, алгоритмы и HTML. Потом верстальщику понадобилось изменить значительную часть HTML кода и вуаля — код поддерживать невозможно, не только верстальщиком, но и самим программистом. Бизнес-логика переплетена в логикой отображения, смешались в кучу кони, люди.

Но оставим новичка в покое. Все PHP-программисты так начинали и автор данной статьи тоже не исключение.

Студент

Рано или поздно веб-программист начинает становиться опытнее и к нему приходит понимание, что отделять HTML-код от PHP-кода всё же нужно. Проштудировав форумы и некоторые руководства (а быть может и по собственной смекалке) программист пишет программу, которая в коде HTML-шаблона, на месте определенных меток типа %var% или {var}, подставляет значения, полученные от PHP-скрипта:

Скрипт script.php:

<?php
$a = isset($_GET['a']) && is_numeric($_GET['a']) ? $_GET['a'] : 0;
$b = isset($_GET['b']) && is_numeric($_GET['b']) ? $_GET['b'] : 0;

$result = $a + $b;

if ($result) {
    $body = "<span style=\"color:blue; font-weight:bold\">Результат: $result</span>";
} else {
    $body = "<span style=\"color:red; font-weight:bold\">Результат равен нулю!</span>";
}

// загружаем содержимое файла шаблона в строку
$tpl = file_get_contents('template.html');
// меняем в шаблоне метку {body} на переменную $body
$html = str_replace('{body}', $body, $tpl);
echo $html;

Шаблон template.html:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
    <title>Основной шаблон HTML-страницы</title>
</head>
<body>
{body}
</body>
</html>

Что получается? Основной каркас страницы лежит в отдельном файле и уже не связан с PHP-кодом, уже лучше. Но в скрипте script.php по прежнему присутствует HTML-код и логика отображения! Получается, променяли "шило на мыло". HTML теперь размазан как по скрипту, так и по основному шаблону, что не очень отличается от кода "Новичка" из примера выше.

Умник

Следующий шаг — это терминальная стадия, когда программист в попытке отделить PHP от HTML начинает писать свой собственный шаблонизатор — набор правил для шаблона, которые могли бы выполнять хотя бы минимальные логические операции с данными, полученными из PHP-скрипта:

Скрипт script.php:

<?php
$a = isset($_GET['a']) && is_numeric($_GET['a']) ? $_GET['a'] : 0;
$b = isset($_GET['b']) && is_numeric($_GET['b']) ? $_GET['b'] : 0;

$result = $a + $b;

// загружаем содержимое файла шаблона в строку
$template = file_get_contents('template.html');
// запускаем наш супер-мега самописный шаблонизатор и передаем в него данные из 
// php-скрипта в виде пар ключ => значение
$html = super_mega_template_engine($template, ['result' => $result]);
echo $html;

Шаблон теперь выглядит так:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
    <title>Основной шаблон HTML-страницы</title>
</head>
<body>
    <if[$result] {<span style="color:blue; font-weight:bold">Результат: [$result] </span>}
                 {<span style="color:red; font-weight:bold">Результат равен нулю</span>}
    </if>
</body>
</html>

В шаблоне появляется IF-подобная конструкция, которую обрабатывает пользовательский шаблонизатор super_mega_template_engine. И вроде бы она работает — выводит значение в зависимости от значения переменной body. Но дальше начинается самое веселое — с усложнением логики отображения требуется создать в шаблонизаторе конструкции, обрабатывающие массивы — циклы. Тривиального IF-подобного синтаксиса начинает не хватать — нужны уровни вложенности и многое другое. В конечном итоге программист приходит на форум и задается вопросом — как написать свой собственный шаблонизатор на PHP.

А все дело в...

А всё дело в упомянутой в начале статьи фразе об отделении кода скрипта PHP от кода HTML шаблона. Так получилось, что огромная программистская общественность в буквальном смысле слова не поняла посыл неизвестного автора — отделять PHP от HTML не нужно! Нужно отделять логику приложения от логики отображения, но это не значит, что в HTML-шаблоне мы не можем использовать PHP-код. PHP изначально задумывался как язык, позволяющий делать вставки кода в HTML страницы:

PHP сконструирован специально для ведения Web-разработок и его код может внедряться непосредственно в HTML — php.net.

Что из этого следует? PHP — сам по себе является не только очень мощным языком программирования, но и самодостаточным шаблонизатором, позволяющим делать качественные шаблоны без ущерба для логики приложения. Для этого надо соблюсти следующие условия:

  • Не использовать в шаблонах логику приложения, передавать в шаблоны только данные, полученные из скрипта — скаляры, массивы, объекты. Никаких вызовов к базе, алгоритмов не связанных с логикой отображения и т.п.
  • Использовать в шаблонах структуры управления PHP, необходимые для логики отображения - IF/ELSEIF/ELSE, FOR/FOREACH, INCLUDE/REQUIRE.
  • Использовать для структур управления альтернативный синтаксис — он очень упрощает чтение HTML-шаблонов.
  • Стараться не использовать встроенные функции в шаблонах. Если даже вам нужно применить в шаблоне довольно часто используемую функцию htmlspecialchars, то не поленитесь обернуть её в статический метод класса-помощника или в функцию. Это в дальнейшем даст больший простор для рефакторинга и просто создаст единобразный стиль вашего API.

Такой стиль шаблонизации на PHP называется pure-шаблонизация, т.е. чистая шаблонизация, основанная на возможностях самого PHP.

Используя pure-шаблонизацию код нашего скрипта и шаблона мог бы выглядеть так:

Скрипт:

<?php
$a = isset($_GET['a']) && is_numeric($_GET['a']) ? $_GET['a'] : 0;
$b = isset($_GET['b']) && is_numeric($_GET['b']) ? $_GET['b'] : 0;

$result = $a + $b;

// загружаем шаблон 
include('template.html');

Шаблон:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
    <title>Основной шаблон HTML-страницы</title>
</head>
<body>
    <?php if ($result): ?>
        <span style="color:blue; font-weight:bold">Результат: <?=$result?></span>
    <? else: ?>
        <span style="color:red; font-weight:bold">Результат равен нулю</span>
    <? endif; ?>
</body>
</html>

Согласитесь, красиво и совершенно просто! Мы разделили логику приложения и логику отображения. Теперь верстальщик, хоть немного знакомый с тривиальными управляющими конструкциями любого языка программирования, может с легкостью поддерживать HTML-код, а программисту нет необходимости что-либо знать о том, как и где будет выведен результат работы программы. Мы разделили обязанности и создали поддерживаемый код, который легко модифицировать.

Конечно, наш пример очень прост, но приемущества pure-шаблонизации очень заметны на реальных проектах.

Пример шаблона посложнее — гостевая книга, вывод записей

В качестве примера посложнее можно привести шаблон гостевой книги, выводящей записи из заранее сформированного массива $guestbook_messages. Выводятся записи как зарегистрированных, так и незарегистрированных пользователей. Кроме того, возможен вывод сообщения администратора гостевой книги (если оно есть) под определенным сообщением пользователя.

В массиве присутствуют следующие ключи:

  • $guestbook_messages['user_id'] — ID зарегистрированного пользователя. Если его нет, значит пользователь — анонимный.
  • $guestbook_messages['user_name'] — Имя зарегистрированного пользователя. Если его нет, значит пользователь — анонимный.
  • $guestbook_messages['user_ip'] — IP-адрес пользователя.
  • $guestbook_messages['user_message'] — Сообщение пользователя.
  • $guestbook_messages['date'] — Дата публикации сообщения.
  • $guestbook_messages['admin_answer'] — Сообщение администратора, относящееся к записи пользователя.
<!DOCTYPE html>
<html>
<head>
<title>Гостевая книга</title>
</head>
<body>

<?php if ($guestbook_messages): ?>
    <?php foreach ($guestbook_messages as $message): ?>
    
        <?php if ($message['user_id']): ?>
            <p class="register_user_info">Пользователь: 
            <a href="/users/<?=$message['user_id']?>.html">
                <?=htmlspecialchars($message['user_name'])?>
            </a>
            </p>
        <?php else: ?>
            <p class="anonim_user_info">Анонимный пользователь с IP <?=$message['user_ip']?></p>
        <?php endif; ?>
        
        <div class="message"><?=htmlspecialchars($message['user_message'])?></div>
        
        <div class="date"><?=date(DATE_W3C, $message['date'])?></div>
        
        <?php if ($message['admin_answer']): ?>
            <div class="answer"><?=htmlspecialchars($message['admin_answer'])?></div>
        <?php endif; ?>
        
    <?php endforeach; ?>
<?php else: ?>

    <p>В гостевую книгу ещё не добавлено ни одной записи</p>
    
<?php endif;?>

</body>
</html>

Экранирование данных, пришедших извне

Отдельного внимания заслуживает тема экранирования данных. Хороший тон веб-программирования — записывать пришедшие от пользователя данные "как есть", а при выводе подвергать их обработке, в зависимости от требуемого формата вывода. Например, если это стандартное веб-приложение, то необходимо экранировать спецсимволы, использующиеся в HTML через функцию htmlspecialchars, что бы предотвратить XSS-уязвимости. Наоборот, для Excel/Word формата нет необходимости применять htmlspecialchars для данных из базы веб-приложения. Т.е. каждый формат, в котором будут выводится данные, диктует свои правила обработки этих данных.

После выхода в свет этой статьи на форуме phpclub.ru было обсуждение — на каком этапе лучше форматировать данные, которые пришли в базу из пользовательского ввода (как пример — сообщения в гостевой книге, которые могут содержать HTML и JavaScript код). Львиная доля разработчиков согласилась с утверждением, что данные лучше форматировать не в php-скрипте, а в шаблоне. Причина тому в том, что форматов вывода данных может быть много, а php-скрипт, получающий данные из базы, как правило один общий. Соответственно знание о том, как форматировать данные должен принимать обработчик, который занимается отображением данных, а не общий PHP-скрипт, генерирующий эти данные.

Как пример — все те же сообщения из гостевой книги пользователя. Пользователь Хакер ввёл в текст сообщения JavaScript код, который бесконечно будет показывать alert-сообщение с надписью "Ты дурак!". Применив htmlspecialchars мы экранировали спецсимволы HTML, что в конечном итоге дало отображение HTML кода как текста. JavaScript код не сработал:

<?php
// Сообщения нашей гостевой книги
$guestbook_messages = array(
    array('name' => 'Вася', 'message' => 'Хороший сайт!'),
    array('name' => 'Хакер', 'message' => '<script>while(1)alert("Ты дурак!")</script>'),
);
?>

<html>
<head>
<title>Моя гостевая книга</title>
</head>
<body>
    <?php foreach($guestbook_messages as $message): ?>
        <p><b><?=htmlspecialchars($message['name'])?></b>:</p>
        <p><?=htmlspecialchars($message['message'])?></p>
        <hr />
    <?php endforeach; ?>
</body>
</html>

Результат отображения в браузере:

Вася:

Хороший сайт!


Хакер:

<script>while(1)alert("Ты дурак!")</script>


Дополнение к оригинальной статье

Описанный выше подход - с использованием встроенных в PHP возможностей шаблонизации - хорошо работает в маленьких, простых скриптах, где не хочется подключать внешние библиотеки. На больших проектах сторонний шаблонизатор позволяет сделать код шаблонов более простым. На данный момент (2017 год) один из самых распространенных шаблонизаторов - Twig, у него удобный синтаксис и много возможностей, которых нет во встроенном шаблонизаторе PHP: наследование шаблонов, автоэкранирование (не нужно вручную писать вызов htmlspecialchars или аналогичной функции), удаление лишних пробелов.

Вот пример простого шаблона на Twig, выводящего комментарии из примера гостевой книги выше:

<body>
    {% for message in guestbook_messages %}
        <p><b>{{ message.name }}</b>:</p>
        <p>{{ message.message }}</p>
        <hr />
    {% else %}
        <p>Сообщений пока нету.</p>
    {% endfor %}
</body>

Также, некоторым нравится альтернативный синтаксис для HTML с отступами - HAML, позволяющий генерировать HTML-разметку, используя меньший объем кода. HAML был придуман для использования с языком Руби, но позже и для других языков были сделаны похожие шаблонизаторы. На основе HAML был создан синтаксис шаблонов для JS под названием Pug (ранее он назывался Jade).

Вот пример HAML-шаблона:

%html
  %head
    %title Название страницы
  %body
    %h1 Заголовок страницы

    #content
      .left.column
        %p= print_information
      .right.column
        = render :partial => "sidebar"

И пример Pug-шаблона:

doctype html
html(lang="en")
  head
    title= pageTitle
  body
    h1 Pug - шаблонизатор для JS
    #container.col
      if youAreUsingPug
        p Вы молодец!
      else
        p Познакомьтесь с Pug!
      p.
        Pug - это лаконичный и простой язык шаблонов, уделяющий 
        внимание производительности и мощным возможностям.

Вместо тегов здесь используется CSS-подобный синтаксис, а вложенность элементов друг в друга задается отступами. Например, конструкция #container.col в коде выше развернется в HTML-код <div id="container" class="col">...</div>. Для PHP есть шаблонизаторы с поддержкой синтаксисов Pug и HAML:

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

Также, довольно распространен синтаксис Handlebars/Mustache.js. Эти шаблонизаторы изначально были придуманы для языка JavaScript, но есть их аналоги на PHP. Вот пример шаблона:

<div class="post">
  <h1>Автор: {{fullName author}}</h1>
  <div class="body">{{body}}</div>

  <h1>Комментарии</h1>

  {{#each comments}}
  <h2>Автор: {{fullName author}}</h2>
  <div class="body">{{body}}</div>
  {{/each}}
</div>

Особенность этих шаблонизаторов в том, что в них очень сильно ограничен набор и возможности управляющих конструкций (вроде if), чтобы в шаблонах был минимум логики. Вот шаблонизаторы на PHP, поддерживающие этот синтаксис:

Также ранее был популярен (но сейчас он используется реже) шаблонизатор на основе языка XSLT. XSLT - это язык, который позволяет преобразовать XML-документ в другой XML- или HTML-документ. Вот пример фрагмента XSLT-шаблона:

<xsl:for-each select="/guestbook-messages/message">
    <p>
        <b>Автор: <xsl:value-of select="@name" /></b>:
    </p>
    <p>
        <xsl:value-of select="@message" />
    </p>
    <hr />    
</xsl:for-each>

А вот пример XML-данных, которые нужно передать в этот шаблон:

<guestbook-messages>
    <message name="Иван" message="Текст сообщения Ивана">
    <message name="Петр" message="Текст сообщения Петра">
</guestbook-messages>

В статье Википедии про XSLT есть гораздо больше примеров.

Преимуществом XSLT является строгость синтаксиса - он не пропустит незакрытые или несбалансированные HTML-теги в шаблоне. К недостаткам относится громоздкость синтаксиса, сложность расширения шаблонизатора пользовательскими функциями, необходимость преобразовывать все данные в XML вместо передачи напрямую в шаблонизатор.

XSLT версии 1 поддерживается стандартным расширением к PHP: http://php.net/manual/ru/book.xsl.php

Для поддержки XSLT версии 2 придется устанавливать сторонние библиотеки: http://www.saxonica.com/html/saxon-c/

Ссылки: