В статье вы не найдёте сравнительных тестов шаблонизаторов. Зато найдёте информацию об использовании xslt в качестве шаблонизатора на реальных проектах. Рассмотрены возможности именованных шаблонов, использование шаблонов-функций, справочников.
1. Структура проекта
Обычно страница сайта состоит из нескольких общих блоков (меню, футер, ...) и контентной части, которую для общности будем называть основным блоком. Все эти блоки размещаются внутри некоторого индексного шаблона, который знает в каком месте какой блок отобразить: меню должно быть вверху, основной блок в центре, а футер внизу.
Получаем следующую структуру
/themes - здесь раполагаются все шаблоны
/themes/index/main.xsl - индексный шаблон
/themes/models/user.xsl - именованные шаблоны, которые относятся к модели пользователя
/themes/inc/functions.xsl - именованные шаблоны-функции
/themes/blocks/footer.xsl - шаблон футера
/themes/blocks/menu.xsl - шаблон меню
/themes/cabinet/main.xsl - шаблон основного блока главной страницы кабинета пользователя
Контроллер главной страницы кабинета пользователя работает следующим образом:
- получает данные для основного блока, обрабатывает их с помощью /themes/cabinet/main.xsl и результат (готовый html) помещает в итоговый xml
- аналогично обрабатывает данные для других блоков (меню, футер) и результат помещает в xml
- итоговый xml, в котором находятся данные всех блоков, обрабатывается с помощью индексного шаблона /themes/index/main.xsl и результат отдаёт пользователю.
Индексный шаблон /themes/index/main.xsl может выглядет следующим образом:
<xsl:template match="page">
<head>
<title><xsl:value-of select="title" /></title>
</head>
<body>
<div class="page-container">
<xsl:value-of select="blocks/menu_top/html" disable-output-escaping="yes"/>
<div class="main">
<xsl:value-of select="blocks/content/html" disable-output-escaping="yes"/>
</div>
<xsl:value-of select="blocks/footer/html" disable-output-escaping="yes"/>
</div>
</body>
</xsl:template>
2. Именованные шаблоны
Шаблон xslt принимает данные в виде xml-документа. Это удобно тем, что мы можем оперировать целыми узлами. Например для вывода имени пользователя у нас может быть такой шаблон
<xsl:template name="inc_show_user">
<xsl:param name="user"/>
<img src="/img/{$user/userpic}.png"/>
<xsl:value-of select="concat($user/first_name, ' ', $user/last_name)"/>
</xsl:template>
который располагается в файле /themes/models/user.xsl.
Мы можем использовать этот шаблон как для отображения текущего пользователя
<xsl:call-template name="inc_show_user">
<xsl:with-param name="user" select="/*/cur_user"/>
</xsl:call-template>
так и для отображения списка пользователей
<xsl:for-each select="users/item">
<xsl:call-template name="inc_show_user">
<xsl:with-param name="user" select="."/>
</xsl:call-template>
</xsl:for-each>
Такое единство отображения сущностей позволяет быстро изменять их отображение. Конечно не у всех пользователей есть картинка, а значит и выводить её нужно не для всех
<xsl:template name="inc_show_user">
<xsl:param name="user"/>
<xsl:choose>
<xsl:when test="$user/userpic>0">
<img src="/img/{$user/userpic}.png"/>
</xsl:when>
<xsl:otherwise>
<img src="/img/default.png"/>
</xsl:otherwise>
</xsl:choose>
<xsl:value-of select="concat($user/first_name, ' ', $user/last_name)"/>
</xsl:template>
3. Импорт шаблонов
Для того чтобы в шаблоне блока иметь доступ к отображению сущности «пользователь» мы должны подключить файл /themes/models/user.xsl.
Для шаблона /themes/cabinet/main.xsl подключение будет выглядеть так
<xsl:import href="../models/user.xsl"/>
(xsl:import должен описываться сразу после xsl:stylesheet)
4. Ни строчки php-кода в представлении
Патерн MVC предполагает разделение модели, логики и представления. Логика приложения запрашивает необходимые данные у модели и передаёт их в представление. Представление должно получить необходимое количество данных, чтобы их отобразить пользователю. Т.е. в представлении мы должны только вывести их и не должны как-либо ещё преобразовывать данные. Мы не должны получать имя пользователя по его id, не должны получать текущее время, и т.д. все эти данные уже должны быть доступны для представления. Если каких-либо данных не хватает, значит контроллер должен их предоставить.
Xslt позволяет производить простейшие операции с данными: сравнение, подсчёт количества, сортировка, форматирование чисел, округление, арифметические операции, конкатенация,… Казалось бы, что это противоречит предыдущему абзацу. Но позвольте заметить, что в результате всех этих операций мы не получаем новых данных, а лишь преобразуем имеющиеся данные.
Не всегда есть все необходимые средства для получения необходимого результата. Например, вывод окончания для числа. Думаю у многих есть подобная функция
function str_plural_form($n, $form1='штука', $form2='штуки', $form5='штук'){
$lastN=$num%10;
$lastT=$num%100;
if($lastT>=10 && $lastT<=20){
return $form5;
}
switch ($lastN){
case 1:
return $form1;
case 2:
case 3:
case 4:
return $form2;
default:
return $form5;
}
}
И даже больше, xslt позволяет вызвать эту функцию прямо из шаблона
<xsl:value-of select="php:function('str_plural_form', 1*$cnt_users, 'пользователь', 'пользователя', 'пользователей')"/>
Но это не только противоречит заголовку раздела, но и является неким атавизмом. Лучше избегать вызовов php-функций внутри xslt-шаблонов.
Что же делать? Есть 2 выхода:
- пусть контролер вызывает str_plural_form и отдаёт нужные данные
- сделать именованный шаблон-функцию, которую мы поместим в /themes/inc/functions.xsl
<xsl:template name="f_plural_form">
<xsl:param name="num"></xsl:param>
<xsl:param name="format">### ###</xsl:param>
<xsl:param name="is_show_num">1</xsl:param>
<xsl:param name="space"/>
<xsl:param name="str1">штука</xsl:param>
<xsl:param name="str2">штуки</xsl:param>
<xsl:param name="str5">штук</xsl:param>
<xsl:if test="$is_show_num=1">
<xsl:value-of select="format-number($num, $format)"/>
<xsl:choose>
<xsl:when test="$space!=''">
<xsl:value-of select="$space" disable-output-escaping="yes"/>
</xsl:when>
<xsl:otherwise>
<xsl:text> </xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<xsl:variable name="lastN" select="$num mod 10"/>
<xsl:variable name="lastT" select="$num mod 100"/>
<xsl:choose>
<xsl:when test="$lastT>=10 and 20>=$lastT">
<xsl:value-of select="$str5" disable-output-escaping="yes"/>
</xsl:when>
<xsl:when test="$lastN=1">
<xsl:value-of select="$str1" disable-output-escaping="yes"/>
</xsl:when>
<xsl:when test="$lastN=2 or $lastN=3 or $lastN=4">
<xsl:value-of select="$str2" disable-output-escaping="yes"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$str5" disable-output-escaping="yes"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Вызов функции будет выглядеть так
<xsl:call-template name="f_plural_form">
<xsl:with-param name="is_show_num">1</xsl:with-param>
<xsl:with-param name="num" select="$cnt_users"/>
<xsl:with-param name="str1">пользователь</xsl:with-param>
<xsl:with-param name="str2">пользователя</xsl:with-param>
<xsl:with-param name="str5">пользователей</xsl:with-param>
</xsl:call-template>
5. Справочники
Вернёмся к выводу информации о пользователе. К примеру на странице форума нам нужно вывести
- список постов с именами пользователей,
- список самых активных пользователей,
- список пользователей которые в данный момент просматривают эту страницу.
Можно решить задачу влоб. При получении каждого из списков делать LEFT JOIN users и получать необходимые данные для вывода информации о пользователе. Но есть и отрицательные моменты такого решения. Первое — возможная избыточность данных (пользователи из списков могут повторяться), второе — дополнительная нагрузка на sql-сервер.
Другой вариант решения задачи. Получить все списки. Затем из этих списков получить набор user_id. И по этому набору сделать один запрос к таблице users. Результат сложить в xml по известному адресу, например /ref_users.
В итоге у нас должен получиться xml-документ с узлами posts, active_users, online_users, ref_users.
Для вывода информации о пользователе сделаем такой именованный шаблон
<xsl:template name="inc_show_user_by_id">
<xsl:param name="user_id"/>
<!-- поиск пользователя в справочнике по его id -->
<xsl:variable name="cur_user" select="/*/ref_users/item[user_id=$user_id]"/>
<xsl:call-template name="inc_show_user">
<xsl:with-param name="user" select="$cur_user"/>
</xsl:call-template>
</xsl:template>
и сохраним его в /themes/models/user.xsl. Это шаблон для вывода пользователя по его id.
Вывести список постов с информацией о пользователе можно так
<xsl:for-each select="posts/item">
<xsl:call-template name="inc_show_user_by_id">
<!-- передаём в шаблон user_id автора поста -->
<xsl:with-param name="user_id" select="user_id"/>
</xsl:call-template>
<!-- далее вывод самого поста -->
<!-- ... -->
</xsl:for-each>
Заключение
Статья получилась объёмной, поэтому не рассмотренными остались вопросы организации шаблонов для ajax, «абстрактные шаблоны», поддержка нескольких языков. А также вопросы скорости и кеширования.
Автор: Holden