Использование xslt-шаблонов в реальных проектах

Источник: habrahabr
Holden

В статье вы не найдёте сравнительных тестов шаблонизаторов. Зато найдёте информацию об использовании 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 - шаблон основного блока главной страницы кабинета пользователя

Контроллер главной страницы кабинета пользователя работает следующим образом:

  1. получает данные для основного блока, обрабатывает их с помощью /themes/cabinet/main.xsl и результат (готовый html) помещает в итоговый xml
  2. аналогично обрабатывает данные для других блоков (меню, футер) и результат помещает в xml
  3. итоговый xml, в котором находятся данные всех блоков, обрабатывает с помощью индексного шаблона /themes/index/main.xsl и результат отдаёт пользователю в виде html.


Индексный шаблон /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 выхода:
  1. пусть контролер вызывает str_plural_form и отдаёт нужные данные
  2. сделать именованный шаблон-функцию, которую мы поместим в /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, "абстрактные шаблоны", поддержка нескольких языков. А также вопросы скорости и кеширования.

Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=30995