Шаблоны в PHP. Часть I.

Если судить по многочисленным темам в различных форумах, посвящённых языку 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>";

$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
$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
$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;

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

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

<!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, часть II