XML: спецификация и функции DOM в PHP
Автор: Дмитрий Лебедев
Иногда хочется отвлечься от
текущей рутины кодирования и от небольших проблем, которым
посвящается место в статьях на "деталях". Окинуть взглядом то,
что делаешь долгое время. Итак, моё видение подходов к
основной задаче php-программирования — генерации
веб-страниц.
Введение: о спецификациях
XML-технологий
Множество разных спецификаций вокруг XML в первую очередь
направлены на то, чтобы упорядочить и привести к единому
стандарту подходы к работе с данными в формате XML. На данный
момент существуют XML + XLink + XSL + пространства имён +
информационное множество + XML Linking + Модель XPointer +
пространства имён XPointer + xptr() XPointer + XSLT + XPath +
XSL FO + DOM + SAX + PI для связи с листом стилей + XML-схема
+ XQuery + Шифрование XML + Канонизация XML + XML-подпись +
DOM уровня 2 + DOM уровня 3 (список взят из статьи "С днем
рождения, XML!").
Впечатление обманчиво, это
математики, высшей кафедры из людей забытых богом вроде чем-то
представляться может и W3C. Поэтому, информации. или
телекоммуникаций компьютеров, рынке на работающей фирме,
крупной какой-либо в должность руководящая работа
их основная когда совместительству, по работают руководстве
люди И будут. скоро либо решения, общего требуют уже которые
задач, решения инструментов методов описания как создаются
Спецификации подобного. Ничего спецификаций. ради спецификации
разрабатывает занимается чем непонятно консорциум что мнение,
распространенное Существует столько. зачем всё же чему к
вопрос, наводит стандартов
Введение: о спецификациях
XML-технологий
Что такое DOM
Document Object Model (объектная модель документа). Объект
в данном случае значит объект в программистском смысле —
артефакт ООП и все прекрасное, за что мы его любим.
Взглянем на исходный код XML-документа:
<?xml version="1.0" encoding="windows-1251"?>
<root language="russian">
<title>XML: спецификация и функции DOM в PHP</title>
<text>Множество разных спецификаций вокруг <acronym>XML</acronym>
в первую очередь направлены на то, чтобы <b>упорядочить</b> и привести к
единому стандарту подходы к работе с данными в формате <acronym>XML</acronym>.
</text>
<date>2003-05-12</date>
<raw-code>
<![CDATA[ <br> пример не well-formed разметки: <p>bla-bla</p> ]]>
</raw-code>
<!-- дописать в статью живые примеры надо бы... -->
</root>
Основа идеологии XML в том, что документ — это набор
узлов древовидной структуры данных. Данный документ можно
представить в виде следующего дерева:
-o- Документ
|
+-o- Элемент root
|
+-o- Атрибут language
|
+-o- Элемент title
| |
| +-o- Текстовый узел ("XML: спецификация...")
|
+-o- Элемент text
| |
| +-o- Текстовый узел ("Множество...")
| |
| +-o- Элемент acronym
| | |
| | +-o- Текстовый узел ("XML")
| |
| +-o- Текстовый узел (" в первую очередь...")
| |
| +-o- Элемент b
| | |
| | +-o- Текстовый узел ("упорядочить")
| |
| +-o- Текстовый узел ("и привести...")
| |
| +-o- Элемент acronym
| | |
| | +-o- Текстовый узел ("XML")
| |
| +-o- Текстовый узел (".")
|
+-o- Элемент date
| |
| +-o- Текстовый узел ("2003-05-12")
|
+-o- Элемент raw-code
| |
| +-o- Секция CDATA ("<br>...")
|
+-o- Комментарий ("дописать...")
|
Знаком "-o-" на схеме обозначены узлы. Справа от них
текст означает тип узла. Для текстовых узлов, секции CDATA и
комментария добавлено содержимое — ради удобства
ориентирования. На самом деле, по-хорошему, переносы строк
между элементами являются текстовыми узлами, и их тоже можно
было бы внести в схему.
Итак, разбираем схему. Всё, что есть в документе —
узлы, и сам документ — тоже узел. Это значит, что есть
класс объектов "узел", а остальные классы ("документ",
"элемент", "текстовый узел", "CDATA", "комментарий") —
дочерние от него и наследуют его свойства и методы. Какие
свойства и методы должны содержаться в каких классах —
описывается в спецификации DOM.
Если посмотреть в документацию
по модулю DOM XML (тоже мне показатель :)), видно, что у
всех этих разных узлов есть много общего — 28 методов у
класса DomNode, а вместе с дочерними классами методов 62. Как
можно догадаться, методы и свойства класса DomNode
присутствуют и в других классах.
На сайте phpPatterns() есть статья "Грубая
схема модуля DOM XML в PHP" Гарри Фьюекса. Тем, кто
по-английски умеет, можно прочитать первоисточник, остальным
даю своё огрубление грубой схемы.
В статье приводится иллюстрация взаимоотношений классов
модуля DOM XML. Вот дерево классов в моём исполнеии:
o- DomNode
|
+-o- DomAttribute
|
+-o- DomCData
| |
| +-o- DomComment
| |
| +-o- DomDTD
| |
| +-o- DomText
|
+-o- DomDocument
|
+-o- DomDocumentType
|
+-o- DomElement
|
+-o- DomEntity
|
+-o- DomEntityReference
|
+-o- DomProcessingInstruction |
Далее приводятся замечания, что модуль DOM XML пока что не
полностью соответствует спецификации (а "левыми" функциями уже
каждый успел попользоваться, теперь во многих приложениях надо
выковыривать их и переписывать код), что ещё много утечек
памяти модуля будет исправлено в версии 4.3.2 (которая ещё не
выпущена и находится в стадии релиз-кандидата). Но это мелочи
жизни. Кто давно пользуется DOM XML, тому не привыкать, а если
вы только начали знакомство с ним, то начнете использовать в
реальных задачах уже тогда, когда он станет стабильным и будет
соответствовать спецификации. В общем, продолжаем знакомство с
DOM и модулем.
Спецификация DOM описывает то, какие объекты должны
присутствовать в приложениях, работающих с XML, какие методы
должны быть у этих объектов и как они должны влиять на узлы
документа. Поэтому в языке Java, Javascript и других системах,
где уже есть поддержка DOM, XML-документы имеют одинаковый
интерфейс, различающийся только названиями функций. Страшно
предположить, что было бы, начни разработчики самостоятельно
изобретать модель.
Работа в PHP с
документом
Поддержка кириллицы
Стандарт предусматривает работу с данными,
перекодированными в UTF-8, поэтому все функции по вводу данных
требуют, чтобы они были перекодированы, а на выходе выдают
тоже UTF-8. Для перекодировки нужно пользоваться функцией
iconv.
Измененная библиотека php_domxml с поддержкой русского
языка доступна на сайте dan.phpclub.net.
Она может создавать объект документа из файла или строки, в
которых в открывающем теге стоит соответствующий
атрибут:
<?xml version="1.0"
encoding="windows-1251"?>
русский
текст
Функция dump_mem в ней тоже выдаёт текст в кодировке
windows 1251, и на этом удобства заканчиваются - остальные
данные нужно вводить в документ, перекодируя в UTF-8.
Создание документа
Объект документа можно создать из существующего файла или
текстовой строки, либо абсолютно новый пустой документ.
<?
$dom1 = domxml_open_file("c:/xml/existing_file.xml");
$dom2 = domxml_open_mem($string);
$dom3 = domxml_new_doc();
?>
Все эти функции при ошибке возвращают не объект, значение
false, так что проверка результата операции достаточно
простая.
По умолчанию при создании документа производится проверка
его синтаксиса (well-form), но не допустимости (соответствие
DTD-схеме или XML-схеме документа, validity). Чтобы проверять
и на допустимость, нужно указать в функции создания документа
(любая из трех приведенных выше) второй, недокументированный
пока, параметр и в нём константу DOMXML_LOAD_VALIDATING:
<?
$dom2 = domxml_open_mem($string, DOMXML_LOAD_VALIDATING);
?>
Получение объекта
элемента
В памяти PHP после того, как документ был создан, хранятся
все объекты элементов документа. Но в переменные скрипта они
без специального вызова не записываются.
Корневой элемент документа можно получить, обратившись к
объекту документа при помощи метода document_element. Функция
возвращает объект класса DomElement, который можно
использовать как аргумент другой функции, либо записать в
переменную:
<?
$root = $dom1->document_element();
?>
Аналогично можно получить любой узел из документа —
при помощи методов объекта документа или объектов
элементов.
<?
// Массив дочерних элементов корневого
$root_child = $root->child_nodes();
for ($i = 0; $i < sizeof($root_child); $i++)
print("$i. ". $root_child[$i]->node_type(). " ". $root_child[$i]->node_name().
"<br/>");
// первый и последний дочерние элементы
$first_child = $root->first_child();
$last_child = $root->last_child();
print($first_child->node_name()." и ".$root_child[0]->node_name()." - одно и то же
");
print($last_child->node_name()." и ".$root_child[sizeof($root_child)-1]->node_name().
" - тоже совпадают
");
// элемент, следующий за первым
// previous_sibling работает точно так же
$second_child = $first_child->next_sibling();
print($second_child->node_name(). " ". $root_child[1]->node_name(). "
");
При разборе дочерних элементов важно следить за типами
узлов, потому что переносы строк, которые ставятся для
удобства чтения и редактирования, тоже становятся узлами
документа и, соответственно, входят в массив дочерних
элементов.
<?
for ($i = 0; $i < sizeof($root_child); $i++)
if ($root_child[$i]->node_type() == XML_ELEMENT_NODE)
// Для иллюстрации здесь текст перекодируется, хотя для латиницы
//это необязательно
$root_child[$i]->set_attribute("makes-sence", iconv("windows-1251",
"UTF-8", "maybe"));
else
print("$i - элемент типа ". $root_child[$i]->node_type());
Впрочем иногда вообще нельзя быть уверенным в том, что
получен объект узла, а не false или null. Тогда, если вызвать
метод объекта, можно получить прямо в результирующий документ
строчку с warning-ом. Чтобы этого избежать, можно проверять
тип элемента функцией get_class.
А неуверенным в результате можно быть, например, когда вы
достаёте нужный элемент из документа при помощи выражений
XPath. Чтобы получить нужный элемент, не имеет смысла, конечно
же, перебирать все элементы документа в его поисках.
специально для этого есть выражения XPath, использующиеся в
XSLT для адресации к узлам преобразуемого документа (атрибуты
select, match).
<?
/* Создание контекста XPath. Аргумент функции - объект документа, в котором выражения
XPath будут выполняться. */
$context = xpath_new_context($dom1);
/* Выполнение выражения и запись результата в переменную result */
$result = xpath_eval($context, "/root/text/acronym");
var_dump($result);
/* Переменная $result - объект класса XPathObject, свойство nodeset - массив,
содержащий объекты полученных элементов. */
for ($i = 0; $i < sizeof($result->nodeset); $i++)
{
$text = $result->nodeset[$i]->first_child();
print(iconv("UTF-8", "windows-1251", $text->node_value()). "
");
}
/* Получение скалярного значения при помощи XPath (подсчёт числа всех элементов в
документе кроме корневого) */
$result = xpath_eval($context, "count(/root//*)");
var_dump($result);
print("
{$result->value}");
Важно помнить про пространства имён XML, которые могут
использоваться в документах. Если вы хотите выполнять
выражения в документах, котоыре содержат элементы из своих
пространств имён (например, XSLT-документы), вам нужно
объявить это проистранство имён. Иначе нельзя будет указывать
имена вида "xsl:template" в выражении.
Адрес (URI) пространства имён в аргументе функции
обязательно должен совпадать с тем, что указан в документе,
иначе XPath-парсер будет считать, что с одним и тем же
префиксом xsl зарегистрированы два разных пространства
имён.
<?
$xslt = domxml_open_file("c:/xml/custom.xslt");
$context = xpath_new_context($xslt);
/* Регистрация пространства имён xsl в контексте XPath */
xpath_register_ns($context, "xsl", "http://www.w3.org/1999/XSL/Transform");
/* Подсчёт количества шаблонов в XSLT-стиле. */
$result = xpath_eval($context, "count(/xsl:stylesheet/xsl:template)");
print($result->value);
Итак, задача получения объекта нужного элемента разобрана.
Теперь о том, что делать с ним.
Копирование
элементов
Пока модуль DOM XML не совсем соответствовал спецификации
DOM, с этим делом была совсем блажь: получаешь объект элемента
и добавляешь его к элементу другого документа. Теперь надо
перед этим клонировать элемент функцией clone_node. Следующий
код копирует элементы из корня первого документа в корень
второго.
<?
$root1 = $dom1->document_element();
$child = $root1->child_nodes();
$root2 = $dom2->document_element();
for ($i = 0; $i < sizeof($child); $i++)
$root2->append_child($child[$i]->clone_node());
Создание новых узлов в
документе
Вы заметили, что здесь пишется то "элемент", то "узел".
Надеюсь, из схемы наследования классов (см. выше) вам стало
понятно, что элемент — это тег, а узел — это более
общее понятие, включающее в себя всё на свете. Стараюсь
употреблять эти слова в нужных местах, чтобы не было
двусмыслия.
Любой узел вставляетс в документ в две операции.
Первая — создание узла. Узел должен быть создан внутри
того документа, в который он будет вставлен. Затем узел
добавляется как дочерний к какому-либо из узлов документа. Для
атрибутов, которые, по-хорошему, тоже узлы есть более удобная
конструкция.
В документации по php, которую я скачивал недавно, есть
список функций, которые были в предыдущих версиях, но не
соответствовали спецификации DOM, и описания их работы. Их
можно изучать, но применять не рекомендуется. С новыми
версиями поведение некоторых конструкций и так менялось,
поэтому полагаться на то, что приговорено к отмене, не
стоит.
<?
/* Корневой элемент добавляется так же, как и другие узлы. */
$dom3 = domxml_new_doc();
/* Функция create_element создаёт узел типа "элемент" */
$root3_new = $dom3->create_element("root");
/* Теперь созданный элемент добавляется к документу. На самом деле, ничто не
мешает отправить результат функции create_element в документ напрямую, а
не через переменную $root3_new. */
$root3 = $dom3->append_child($root3_new);
$title = $root3->append_child($dom3->create_element("title"));
/* Функция create_text_node создаёт текстовый узел. Его добавим как содержимое
элемента title. Сохранять добавленный элемент в переменную необязательно -
только если вы хотите с ним после добавления работать. */
$title->append_child($dom3->create_text_node("Создание новых узлов в документе"));
Аналогично создаются и вставляются в документ узлы других
типов.
Изменение узлов
Формально, таких методов... не предусмотрено. У атрибутов
метод, изменяющий содержимое, есть. Есть элементы, в которых
нет элементов дочерних, а есть только секции CDATA, текст с
сущностями, либо комментарий. Изменять таковые можно путём
удаления существующих узлов и вставки новых. Для элементов,
имеющих дочерние вперемешку с текстовыми, метод редактирования
был бы вообще нонсенсом.
Атрибуты создаются и изменяются через методы объектов
элементов, в которых эти атрибуты содержатся. По спецификации
они так же должны создаваться и добавляться в элементы через
функции create_attribute и append_child, но это всё ещё не
реализовано в PHP 4.3.1 (4.3.2).
<?
// Можно установить значение атрибута через объект его элемента
$root3->set_attribute("language", "Russian");
// Так написано в документации, но не работает.
$root2->append_child($dom2->create_attribute("language", "Russian"));
?>
Объяснение,
почему не работает — "не сделано пока что". Заодно
предлагается пользоваться последней версией из CVS — вот
не было печали; понятно, конечно, что DOM XML — это уже
не для средних умов, но чтобы его нужно было добывать вот
так — увольте. Так же предлагается использовать функцию
set_attribute_node. В некоторых случаях это вызовет
неудобство, когда, например, тип вставляемого в элемент узла
заранее неизвестен — то ли будет сделан текст, то ли
атрибут, и надо бы использовать одну функцию, а пока так
нельзя, пришлось бы делать конструкцию if-else.
Замена одного текстового узла — это удаление
существующего и вставка нового. Если нет уверенности, что в
элементе только текстовый узел и нет других, можно клонировать
элемент, вставить его в своего родителя и потом удалить
исходный. Получится "чистый" элемент без дочерних, правда
атрибуты при этом не скопируются.
<?
// $target - переменная с изменяемым элементом
// Получаем родительский узел.
$parent = $target->parent_node();
// Вставляем в него клон нужного нам узла.
$new_target = $parent->append_child($target->clone_node(false));
// Удаляем старый элемент.
$parent->remove_child($target);
// Вставляем в новый элемент нужный текст.
$new_target->append_child($dom->create_text_node(iconv("windows-1251",
"UTF-8", "Замена узла - это удаление существующего и вставка нового.")));
С изменением текстовых узлов или секций CDATA в сложной
комбинации элементов тоже несложно: получаем нужный объект,
добавляем перед ним новый узел, а старый удаляем.
<?
$new_node = $target_node->insert_before($dom->create_text_node(iconv("windows-1251",
"UTF-8",
"Замена узла - это удаление существующего и вставка нового.")),
$target_node);
$parent = $target_node->parent_node();
$parent->remove_child($target_node);
2 последних строки можно было заменить одной —
$target_node->unlink_node(), но, поскольку эта функция не
соответствует стандарту, она может быть удалена и,
соответственно, в примерах её лучше не использовать.
XSL-Трансформации в
модуле DOM XML
XSLT — это тоже XML-документ. Он читается из файла
(строки) — точно так же, как создаётся объект
документа — или создаётся из объекта XML-документа. Затем
вызывается метод process с объектом трансформируемого
документа в качестве аргумента, и на выходе получается объект
XML-документа.
<?
$xslt = domxml_xslt_stylesheet_doc("c:/xml/custom.xslt");
$dom = domxml_open_file("c:/xml/existing_file.xml");
$final = $xslt->process($dom);
print($final->dump_mem());
Заключение
Объектный подход к документу — это шаг вперед, наше
светлое будущее. Модуль DOM XML предоставляет программный
интерфейс с такими возможностями, каких не сделать на SAX-парсере
в php-скрипте. Он устраняет неопределенности, связанные с
трактовкой символов в сделанном вами XML-документе при его
разборе XSLT-процессором или иным обработчиком. К примеру,
проблемы с вставкой текста в элементы документа в DOM XML не
существует, тогда как при работе с текстом документа нужно
проверять и фильтровать служебные символы. Текст в
DOM-объекте — это текст, а вот если его просто вставить в
строку документа, где символы < и > превратятся в теги,
а сущности, если не объявлены, могут вызвать ошибку.
Да, модуль DOM XML ещё сырой. Не все функции реализованы,
последний релиз всегда будет оставать от того, что находится в
CVS
у разработчиков. Документация сильно отстаёт и от релизов.
Однако разработчики активно общаются с пользователями, модуль
открыт для нововведений и улучшений. Поэтому осваивать его
функции нужно уже сейчас, чтобы к первому коммерческому
проекту, в котором будет использоваться XML, у вас был багаж
знаний и опыт построения простых сайтов с XML.
Огромная благодарность хочет выразиться Денису Жолудову aka [DAN], Евгению
Климову aka Slach, Sababa (кого забыл —
извините) за помощь лично и на форуме, за подробные
разъяснения и долготерпение. Не пинайте ногами, пожалуйста,
это для неофитов написано! :)
Ссылки по теме
- Статья, рассматривающая основные проблемы, с
которыми сталкиваются XSLT-программисты
- Класс "SQL 2 XML" — выдача результатов
запросов к базам данных в XML
- XMLHack.ru — много информации об
XML-технологиях на русском языке
- Мой проект по созданию формообработчика,
использующего XML для описания форм.
- Dan.phpclub.net — библиотека php_domxml с
поддержкой кириллицы и многое другое