Преобразование XML + XSLT с помощью Sablotron
Автор: Дмитрий Лебедев
Альтернативное введение в
использование XSL Transformations в PHP при помощи
Sablotron.
Данный материал следует воспринимать как альтернативное
введение в использование XSLT с Sablotron в PHP.
Термины XSL и XSLT близки друг к другу, и новичкам их можно
считать синонимами. Подробности, в чём же различия, описаны в спецификации
XSL Transformations W3C.
Все, кто интересовался возможностями XSLT, читал
стандартный пример из мануала, либо примеры, приводимые в
статьях, посвящённых XSLT, на разных сайтах. Работающий пример из этой же серии:
<?php
$xmlData = '<?xml version="1.0" encoding="Windows-1251"?>
<document>
<game>
<title>Railroad Tycoon II Platinum</title>
<genre>экономическая стратегия</genre>
<designer>PopTop software</designer>
<publisher>G.O.D. games</publisher>
<year>2001</year>
</game>
<game>
<title>Grand Prix 4</title>
<genre>автосимулятор</genre>
<designer>Geoff Crammond & Simergy</designer>
<publisher>Infogrames Entertainment</publisher>
<year>2002</year>
</game>
</document>';
$xslData = '<?xml version="1.0" encoding="windows-1251"?>
<!DOCTYPE xsl:stylesheet>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes" encoding="Windows-1251"/>
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="document">
<html><head>
<title>Игры</title>
</head>
<body>
<h1>Игры</h1>
<table cellpadding="2" cellspacing="2" border="1">
<tr>
<td>Название</td>
<td>жанр</td>
<td>год</td>
<td>разработчик</td>
<td>издатель</td>
</tr>
<xsl:apply-templates select="game"/>
</table>
</body></html>
</xsl:template>
<xsl:template match="game">
<tr>
<td><b><xsl:value-of select="title"/></b></td>
<td><xsl:value-of select="genre"/></td>
<td><xsl:value-of select="year"/></td>
<td><xsl:value-of select="designer"/></td>
<td><xsl:value-of select="publisher"/></td>
</tr>
</xsl:template>
</xsl:stylesheet>';
$xh = xslt_create();
$arguments = array(
'/_xml' => $xmlData,
'/_xsl' => $xslData
);
$result = @xslt_process($xh, 'arg:/_xml', 'arg:/_xsl', NULL, $arguments);
if ($result)
print ($result);
else {
print ("There was an error that occurred in the XSL transformation...\n");
print ("\tError number: " . xslt_errno($xh) . "\n");
print ("\tError string: " . xslt_error($xh) . "\n");
exit;
}
?>
Подобных примеров в Сети полно. Все они хорошо показывают,
что XSL-трансформация в php работает, но после их прочтения
остаётся неясным, зачем XSL нужен, скорее даже наоборот —
почему XSL не нужен.
"Действительно", — подумает читатель, — "если
данные лежат в базе, зачем городить огород, формируя сперва
XML, а затем ещё преобразовывать через XSL? С тем же успехом
это сделает класс HTML-шаблона."
После этого разочарованный программист напрочь теряет
интерес к XSL и вешает на технологию ярлык "ненужная заумь".
Вам, уважаемые читатели, повезло найти такой замечательный
сайт, как "php в деталях". Здесь вы прочитаете о том, что XSL
может не только преобразовывать XML в HTML, но и то, как можно
при помощи XSL облегчить работу с php-скриптами.
Начало работы
Приведённый выше пример, хоть и слишком прост, хорошо
иллюстрирует, каким образом делается XSL-преобразование в php.
Чтобы этот код работал, нужно установить XSLT-процессор
Sablotron. На виндовой машине это делается так:
- положить iconv(-1.3).dll, expat.dll и sablot.dll в
C:\windows\System (все файлы есть в стандартном дистрибутиве
php)
- открыть C:\windows\php.ini и в нём найти параметр
extension_dir. Если значение параметра — "." или нечто
вроде "./", исправить на, скажем, "f:\usr\local\php\extension"
(или адрес директории, в которой у вас лежат/будут лежать
расширения php). Теперь это будет директория расширений php.
- положить в директорию расширений файл php_xslt.dll (это
для php версии 4.2.x), либо php_sablot.dll (для версии 4.0.x)
- в php.ini раскомментируйте строчку extension=php_xslt.dll
(4.2.x) или extension=php_sablot.dll (4.0.x)
Это была краткая инструкция по установке. Я вам этого не
говорил! С вопросами по установке просьба идти в специальный
форум.
Теория
Как я уже писал ранее в статьях от 19.06.2001, 28.08.2001 и 29.05.2002,
использование XSLT позволяет отделить от php-скриптов работу
по форматированию и представлению данных. Это не только
уменьшение объёма кода, но и вынос большого количества
логических конструкций (if, else, switch), а следовательно,
облегчение работы по написанию и отладке программ. Смею
утверждать, что тот, кто не пробовал работать с XSLT, не
представляет себе, насколько php-кодирование облегчится.
Впрочем, не надо обольщаться: если у вас было несколько
конструкций if ... else в php-скрипте, они, скорее всего,
появятся в том же количестве в XSL-файле.
Теперь к примерам.
Вывод списков
Все усложнения, происходящие от необходимости выводить
список в удобочитаемом виде, переносятся на плечи XSL. Пример
#2. Список статей на сайте с подсветкой статьи, которую читают
сейчас, чередование цвета в строках и нумерация списка.
XML:
<current-date>2002-05-30</current-date>
<list-article date="2002-10-03">Ловля ошибок в PHP</list-article>
<list-article date="2002-10-02">Живой проект и мёртвый журнал</list-article>
<list-article date="2002-06-03">Работа с MySQL. Часть 7. Деревья</list-article>
<list-article date="2002-05-30">Ручная сортировка в веб-интерфейсе</list-article>
<list-article date="2002-05-29">Как поладить дизайнеру с программистом</list-article>
<list-article date="2002-05-27">Relax this is PHP</list-article>
XSLT:
...
<table>
<xsl:apply-templates select="list-article"/>
</table>
...
<xsl:template match="list-article">
<tr>
<xsl:if test="position() mod 2 = 1">
<xsl:attribute name="bgcolor">#cccccc</xsl:attribute>
</xsl:if>
<td>
<xsl:value-of select="position()">
<a href="/{@date}.htm"><xsl:value-of select="."/></a>
<xsl:if test="@date = ../current-date"> <</xsl:if>
</td>
</tr>
</xsl:template>
Произвольная
разметка
Переводя на XML сайт с текстами (как этот), естественно
хотеть сделать собственную разметку статей. Например, в
контейнером important выделять очень важные места и иметь
возможность выделять их не обязательно жирным шрифтом, но,
может быть, цветом, CSS-стилем. Или писать цитаты как
<quote>текст цитаты<quote> и иметь возможность
менять стиль их оформления вместе с дизайном сайта.
Медленно продвигаясь от самого простого первого
примера, многие натыкаются на эту проблему и не могут
найти решения. Ведь если выделить абзац в тег <para> и
делать для него шаблон, на первый взгляд, существуют три
способа вывода содержимого:
тег xsl:value-of выводит текст, но удаляет все теги в
абзаце
тег xsl:copy-of выводит копию всего содержимого (без
возможности применять шаблоны к детям — внутренним тегам)
и самого контейнера <para>...</para> (что не очень
красиво в HTML).
наконец, xsl:apply-templates применит шаблоны к детям, но
пропустит текст
Проблема кажется безвыходной, но решение есть. Я использую
"магические" шаблоны, которые выводят и текст и теги в нём со
всеми атрибутами и без изменений. Пример #3:
XML:
<text>
<para>Данный пример использует <strong>магические шаблоны</strong>
для разбора произвольной разметки. Это позволяет избежать таких жалоб:
</para>
<quote>Люди, памажите сами мы не местные! Не могу вывести теги в тексте
при помощи value-of!
</quote>
<hr/>
<strong>Запомните эти шаблоны раз и навсегда!</strong>
<para>Тогда вы сможете обрабатывать <u>любой</u> <a href="http://www.txt.ru">текст</a>
Почти любой.
</para>
</text>
XSLT:
<xsl:template match="text"><xsl:apply-templates/></xsl:template>
<xsl:template match="strong">
<font color="#cc0000"><b><xsl:apply-templates/></b></font>
</xsl:template>
<!-- три магических шаблона -->
<!-- 1. общий -->
<xsl:template match="*">
<xsl:copy>
<xsl:apply-templates select="@*" />
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<!-- 2. для текста -->
<xsl:template match="text()">
<xsl:value-of select="." disable-output-escaping="yes"/>
</xsl:template>
<!-- 3. для тегов и аттрибутов -->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
Первым делом XSLT-процессор при вызове инструкции
apply-templates ищет шаблон для каждого элемента. Для элемента
strong шаблон есть, и именно в соответствии с ним такие
элементы будут обработаны. Для гиперссылки шаблона нет,
поэтому она будет выведена, как есть. Можно добавить в XSL
шаблон и для ссылки, который бы выводил рядом с каждой
текстовой ссылкой картинку для открытия её в новом окне:
<xsl:template match="a[@href]">
<xsl:copy-of select="."/> <a href="{@href}"
target="_blank"><img src="/window.gif" width="15" height="15"
alt="открыть в новом окне"/></a>
</xsl:template>
* в шаблоне использован параметр match="a[@href]" —
этот шаблон будет применён только к тем тегам ссылок, в
которых есть поле href и пропустит якоря (<a
name="xxx"></a>).
Невалидный код и
Кажущаяся необходимость писать валидный XML-код так же
отпугивает многих неофитов XSLT. Хорошо, с завтрашнего дня
будем писать статьи только валидно, благо дома можно
проверить, нет ли в тексте XML-ошибки — mismatched tag
или invalid token, — с этим как-нибудь справимся. Но
ведь, по-хорошему, нужно и весь архив перевести в валидный
код! И я так тоже думал, когда появилась возможность
переделывать сайт на XML.
Решение проблемы довольно простое: не хочешь — не пиши
валидно. Пиши, как привык, — без кавычек в атрибутах
тегов, используй простой <br> и прочее. Достаточно
заключить текст в контейнер <![CDATA[ ... ]]> (пример
ниже).
Что касается , то здесь дела такие: элемента nbsp
в XML нет. Есть lt, gt, quot, но не nbsp (вполне
логично — это ведь non-braking space, который относится к
форматированию и придуман для HTML). Поэтому его нужно
объявить в документе, либо использовать только внутри
<![CDATA[...]]>.
Пример #4:
XML:
<text>
<bad-markup><![CDATA[В этом <a href=http://detail.phpclub.net>тексте</a> применена
невалидная разметка. <br> И ничего страшного.]]></bad-markup>
<quote>Люди, памажите, сами мы не местные!</quote>
<hr/>
<strong>Запомните и эти шаблоны тоже!</strong>
</text>
XSLT:
<xsl:template match="text"><![CDATA[ >>> и в XSL можно делать то же самое! <<< ]]>
<xsl:apply-templates/></xsl:template>
<xsl:template match="bad-markup">
<xsl:value-of select="." disable-output-escaping="yes"/>
</xsl:template>
<xsl:template match="*">
<xsl:copy>
<xsl:apply-templates select="@*" />
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="text()">
<xsl:value-of select="." disable-output-escaping="yes"/>
</xsl:template>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
Очень удобно! Большие изменения в архив вносить не
придётся. Можно начать писать валидно, а продолжать как
попало. А можно комбинировать эти два подхода. Чтобы не писать
в архивные файлы тег CDATA, я сделал простое преобразование
при помощи регулярных выражений (важно так же помнить, что
один тег CDATA не должен содержать в себе другой).
$doc =
preg_replace("~<(p|h[1-3]|pre)>(.*?)</\\1>~",
"<\\1>\\2</\\1>", $doc);
Циклы
Допустим, нам нужно сделать форму для редактирования
статьи, в том числе её даты. Для удобства пользования надо
сделать три раскрывающихся списка (далее —
"крутилки") — дата от 1 до 31, месяц, год. Первое
решение, которое приходит в голову — сделать HTML-код
крутилок в php, вставить в XML в контейнере CDATA, а затем
вывести в XSL с параметром disable-output-escaping="yes".
На самом деле, XSLT может и это. Достаточно вставить в
данные XML число, номер месяца и год. Крутилки можно
нарисовать сразу в XSLT.
Напишем шаблон, не предназначенный ни для какого элемента
документа. Он будет вызываться командой xsl:call-template и
получать два параметра: значение счётчика и максимум. Сперва
он будет выводить нужные нам данные со значением счётчика,
затем вызывать самого себя с параметрами максимум и счётчик,
увеличенный на 1. Пример #5:
XML:
<month-name>Январь</month-name>
<month-name>Февраль</month-name>
<month-name>Март</month-name>
<month-name>Апрель</month-name>
<month-name>Май</month-name>
<month-name>Июнь</month-name>
<month-name>Июль</month-name>
<month-name>Август</month-name>
<month-name>Сентябрь</month-name>
<month-name>Октябрь</month-name>
<month-name>Ноябрь</month-name>
<month-name>Декабрь</month-name>
<article>
...
<day>7</day>
<month>10</month>
<year>2002</year>
</article>
XSLT:
<xsl:template match="article">
...
<select name="d">
<xsl:call-template name="day">
<xsl:with-param name="count">1</xsl:with-param>
</xsl:call-template>
</select>
<select name="m">
<xsl:call-template name="month">
<xsl:with-param name="count">1</xsl:with-param>
</xsl:call-template>
</select>
...
</xsl:template>
<xsl:template name="day">
<xsl:param name="count"/>
<option value="{$count}">
<xsl:if test="$count = //artcile/day">
<xsl:attribute name="selected">yes</xsl:attribute>
<xsl:if>
<xsl:value-of select="$count"/>
</option>
<xsl:if test="$count < 31">
<xsl:call-template name="day">
<xsl:with-param name="count">
<xsl:value-of select="$count + 1"/>
</xsl:with-param>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="month">
<xsl:param name="count"/>
<option value="{$count}">
<xsl:if test="$count = //artcile/month">
<xsl:attribute name="selected">yes</xsl:attribute>
<xsl:if>
<xsl:value-of select="//month-name[position() = $count]"/>
</option>
<xsl:if test="$count < 12">
<xsl:call-template name="month">
<xsl:with-param name="count">
<xsl:value-of select="$count + 1"/>
</xsl:with-param>
</xsl:call-template>
</xsl:if>
</xsl:template>
Оставляю вам в качестве домашнего задания шаблон для вывода
крутилки с годом.
Резюме
Как видите, многое из того, что пишется в php-скриптах,
даже при использовании класса шаблона, можно успешно спустить
в XSLT. Но стОит ли заниматься этим?
Ответ зависит от условий работы в вашем проекте. О
разделении функций формирования и представления данных я уже писал.
Второй момент — технологичность работы.
Допустим, я захочу сделать на этом сайте меню быстрой
навигации — раскрывающийся список со всеми
статьями, — чтобы пользователь мог выбрать статью из
списка и сразу перейти к ней. Ещё я захочу оставить список
последних материалов (сейчас он находится справа вверху).
Если делать это при помощи класса шаблона типа
FastTemplate, нужно два специальных блока и дополнительный код
в php, который бы объявлял в шаблоне блок для списка всех
статей и отдельно блок для списка 10 последних. Аналогичные
действия необходимы в таком случае и при работе без класса
шаблона. При работе с XML достаточно всего лишь одного набора
данных "Дата => Статья", из которого в XSL-документе
строятся и листбокс быстрого перехода, и список последних
статей.
А если вдруг понадобится неважно для чего сделать другое
оформление сайта (например, версия для WAP, или просто
редизайн), в котором будет решено отказаться от списка 10
последних материалов. В случае первых двух технологий —
класс шаблона и смешанный код — нужно будет убрать часть
php кода, в случае XSLT изменения коснутся только XSL-файла.
Такой процесс более технологичен, поскольку невозможно сделать
новые ошибки в php-скриптах (а теперь представьте обратный
случай — списка 10 последних статей не было, но его
решили добавить!).
Итак, выбор остаётся за вами, а я как мог привёл сильные
стороны технологии и доводы в пользу использования XML в
проектах.