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

в 10:50, , рубрики: php, xslt, метки: ,

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


Индексный шаблон /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, «абстрактные шаблоны», поддержка нескольких языков. А также вопросы скорости и кеширования.

Автор: Holden

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js