Если судить по многочисленным темам в различных форумах, посвящённых языку PHP, то можно с уверенностью констатировать, что львиная доля не только новичков, но разработчиков среднего уровня абсолютно не понимают что такое шаблоны в PHP, зачем они нужны и как правильно использовать шаблонизацию в PHP. Именно для тех, кто ещё не до конца осознал, что такое шаблоны и написана эта статья. В ней описаны все критические ошибки, которые совершают начинающие разработчики по части шаблонизации в языке PHP.
Истоки шаблонизации
Давным-давно, после появления 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>";
$result = $a + $b;
if ($result == 0) {
echo "<span style=\"color:red; font-weight:bold\">Результат равен нулю</span>";
} else {
echo "<span style=\"color:blue; font-weight:bold\">Результат: $result</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
$result = $a + $b;
if ($result == 0) {
$body = "<span style=\"color:red; font-weight:bold\">Результат равен нулю</span>";
} else {
$body = "<span style=\"color:blue; font-weight:bold\">Результат: $result</span>";
}
// загружаем содержимое файла шаблона в строку
$tpl = file_get_contents('template.html');
// меняем в шаблоне метку {body} на переменную $body
$tpl = str_replace('{body}', $body, $tpl);
echo $tpl;
Шаблон 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
$body = $a + $b;
// загружаем содержимое файла шаблона в строку
$tpl = file_get_contents('template.html');
// запускаем наш супер-мега шаблонизатор и передаем в него данные из php-скрипта
// в виде пар ключ => значение
$tpl = super_mega_template_engine( array('body' => $body) );
echo $tpl;
Шаблон теперь выглядит так:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Основной шаблон HTML-страницы</title>
</head>
<body>
<if[$body] {<span style="color:blue; font-weight:bold">Результат: {$body} </span>}
{<span style="color:red; font-weight:bold">Результат равен нулю</span>}
</if>
</body>
</html>
В шаблоне появляется IF-подобная конструкция, которую обрабатывает пользовательский шаблонизатор super_mega_template_engine. И вроде бы она работает - выводит значение в зависимости от значения переменной body. Но дальше начинается самое веселое - с усложнением логики отображения требуется создать в шаблонизаторе конструкции, обрабатывающие массивы - циклы. Тривиального IF-подобного синтаксиса начинает не хватать - нужны уровни вложенности и многое другое. В конечном итоге программист приходит на форум и задается вопросом - как написать свой собственный шаблонизатор.
А все дело в...
А всё дело в упомянутой в начале статьи фразе об отделении кода скрипта PHP от кода HTML шаблона. Так получилось, что огромная программистская общественность в буквальном смысле слова не поняла посыл неизвестного автора - отделять PHP от HTML не нужно! Нужно отделять логику приложения от логики отображения, но это не значит, что в HTML-шаблоне мы не можем использовать PHP-код. PHP изначально задумывался как язык, позволяющий делать вставки кода в HTML страницы:
PHP сконструирован специально для ведения Web-разработок и его код может внедряться непосредственно в HTML - php.net.
Что из этого следует? PHP - сам по себе является не только очень мощьным языком программирования, но и самодостаточным шаблонизатором, позволяющим делать качественные шаблоны без ущерба для логики приложения. Для этого надо соблюсти следующие условия:
Не использовать в шаблонах логику приложения, передавать в шаблоны только данные, полученные из скрипта. Никаких вызовов к базе, алгоритмов не связанных с логикой отображения и пр.
Использовать в шаблонах все имеющиеся структуры управления PHP.
Использовать для структур управления альтернативный синтаксис - он очень упрощает чтение HTML-шаблонов.
Стараться не использовать встроенные функции в шаблонах. Если даже вам нужно применить в шаблоне довольно часто используемую функцию htmlspecialchars, то не поленитесь обернуть её в статический метод класса-помощника или в функцию. Это в дальнейшем даст больший простор для рефакторинга и просто создаст единобразный стиль вашего API.
Такой стиль шаблонизации на PHP называется pure-шаблонизация, т.е. чистая шаблонизация, основанная на возможностях самого PHP.
Используя pure-шаблонизацию код нашего скрипта и шаблона мог бы выглядеть так:
Скрипт:
<?php
$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 многословен. Только для того чтобы просто вывести переменную необходимо не менее 14 символов (нет, использование более компактного <?= неприемлемо):
<?php echo $var ?>
Первое. Конструкцию <?php ?> подставляет любой PHP-редактор автоматически при написании первого симола "меньше" (<). Вам даже не нужно набирать руками эти 8 символов.
Второе:
Начиная с PHP 5.4 короткий тег echo <?= всегда распознается и действует, несмотря на значение опции short_open_tag — php.net
Так что любые разговоры связанные с тем, что в PHP исключат короткие теги уже можно, наконец, заканчивать. Теперь можно смело писать шаблоны с использованием коротких тегов:
<html>
<p>Привет, <?=$name?>! Тебе <?=$age?> лет.</p>
</html>
...и PHP становиться нелепо многословен при необходимости экранировать вывод (да, экранирование данных полученных из небезопасного источника является обязательным)
<?php echo htmlspecialchars($var, ENT_QUOTES, 'UTF-8')?>
Сравнити это с двумя примерами написанными на Django:
{{ var }}
{{ var|escape }}
Используя "чистую" PHP-шаблонизацию вам не обойтись без специфических для шаблона классов или функций. Описанная выше проблема многословности элементарно решается введением функции или метода в классе-помощнике. В моём фреймворке это сделано примерно так:
// Класс View представляет собой хранилище данных Вида и занимается
// преобразованием PHP-шаблонов посредством буферизации вывода.
class View {
/**
* Метод возвращает результат работы функции htmlspecialchars
* с параметром ENT_QUOTES (преобразуются и двойные, и одиночные кавычки).
*
* @param string
* @return string
*/
public function e($in)
{
return htmlspecialchars($in, ENT_QUOTES);
}
Соответственно в шаблоне все данные выводятся проходя через этот метод:
<html>
<p>Привет, <?=$this->e($name)?>! Тебе <?=$this->e($age)?> лет.</p>
</html>
Вообще, все разговоры о "многословности" напоминают экономию на спичках и желание придраться за отсутствием должных аргументов. Мы, программисты, пишем тысячи строк программного кода и разговаривать о каких-то восьми символах, из-за которых PHP не может использоваться как шаблонизатор, как-то просто глупо, согласитесь.
Синтаксис ориентированный на шаблоны
По больше части это дело вкуса, но шаблонизаторы должны иметь красивые решения частых случаев. Для примера предположим, что необходимо отобразить массив и вывести текст по умолчанию, если этот массив пуст. Это очень распространенный случай, но версия на PHP не очень читаема:
<?php if ($items): ?>
<?php foreach ($items as $item): ?>
* <?php echo $item ?>
<?php endforeach; ?>
<?php else: ?>
No item has been found.
<?php endif; ?>
Версия на Django гораздо лучше — спасибо конструкции else для тега for:
{% for item in items %}
* {{ item }}
{% else %}
No item has been found.
{% endfor %}
Версия на PHP "не очень читаема"?! Хоть убейте, но я не вижу здесь ничего не читаемого! Наоборот, мы явно видим логику отображения. Но самое интересное в том, что в данном конкретном примере выводится какой-то текст со звёздочкой в виде имитации списка, а не валидный HTML-список. Давайте перепишем данный пример правильно, добавив теги <ul> и <li>.
На PHP это будет выглядеть так:
<?php if ($items): ?>
<ul>
<?php foreach ($items as $item): ?>
<li><?php echo $item ?></li>
<?php endforeach; ?>
</ul>
<?php else: ?>
No item has been found.
<?php endif; ?>
Как в этом случае должна выглядеть версия на Django? Подозреваю, что как-то так:
{% if items %}
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
{% else %}
No item has been found.
{% endif %}
Как видно, разница крайне несущественна разницы никакой.
Безопасность
Я не говорю что PHP небезопасен. Но я хочу сказать что экранирование переменных в шаблоне это просто кошмар, как и продемонстрировал ранее:
<?php echo htmlspecialchars($var, ENT_QUOTES, 'UTF-8') ?>
Конечно, вы можете написать свою функции и сделать ее котороче, но это не то о чем я говорю:
<?php echo e($var) ?>
Я считаю что безопасность должа быть на уровне по умолчанию, особенно для шаблонов, которые зачастую пишут не программисты, которые не обращают внимания на возможные XSS и CSRF уязвимости.
Как показывает практика, шаблоны верстаются верстальщиками и отдаются программистам, которые и "натягивают" шаблоны на веб-приложение. Если программист не экранирует данные в шаблоне, то это проблема исключительно программиста, организации его работы, привычек и пофесионального уровня. А никак не языка и не технологии.