Профессиональная разработка Web-приложений.  
Боишься нашего дизайна?
Новости
PDF журнал
Участники проектa
Сотрудничество
Ссылки
Карта сайта
Комментарии
Комментарии к статье
Добавить комментарий
Обсудить на форуме
Информация об авторе
Оценка статьи

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!").

Введение: о спецификациях 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() недавно (9.4.3) появилась статья "Грубая схема модуля 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 с поддержкой кириллицы и многое другое



  • For comment register here
       2003-05-12 10:30
    Если честно, то не очень впечатляет. В статье хотелось бы увидеть применение ДОМ-а, т.е. _реально_, чем он упрощает (или улучшает) работу.
    XSLT - понятно, разделение структуры документа с его оформлением.
    А DOM?
    Я не спорю, возможно я чего то не понимаю, поэтому и пишу здесь. Я просто пока что вижу в нем доп. грабли в самом модуле, + увеличение размеров кода (рукописания).
    Просветите?

    Да, и кто такие эти "неофиты", для которых это все написано?
    Спасибо.

       2003-05-12 10:55
    Неофиты - те, кто не знаком или мало знаком с DOM XML.

    Как улучшает работу - будет написано в следующих статьях, цель данной была лишь подробно описать суть спецификации и функциональность.

       2003-05-12 15:03
    Кстати, не обязательно юзать iconv. Достаточно utf8_encode(decode)

       2003-05-12 15:08
    В форуме обсуждалось, что эти функции немного странно работают, и вроде как iconv стабильнее. У меня utf8_encode глючила. В общем, надо специально тестировать.

       Unknown 2003-05-12 15:13
    Эта функция работает только с ISO-8859-1

       2003-05-13 14:37
    уже достаточно давно появился метод DomXsltStylesheet->result_dump_mem()

    так что в разделе XSL-трансформация, надо дописать, если у вас PHP версии > 4.3.* (точно не помню когда появилось), то:

    $final = $xslt->process($dom);

    echo $xslt->result_dump_mem($final);

       2003-05-14 09:07
    Хм... и что нового приносит метод result_dump_mem? Ведь в $final записан итоговый документ, а его отдают в метод объекта XSLT ещё раз?

       2003-05-14 19:50
    Писать нужно

    $result= $xslt->xslt_result_dump_mem($doc);
    либо
    $xslt->xslt_result_dump_file($doc,$fp,[compression_flag]);

       2003-05-14 19:58
    Функция dump_mem() возвращает XML вне зависимости от метода, указаного в xsl:output. Для получения HTML можно воспользоватся html_dump_mem()

       2003-05-15 13:35
    Может я чего не понял, но, по крайней мере, у меня нифига не глючит на cp1251 и кою тоже сторит нормально. Другое дело, что владетели других кодингов могут поиметь траблы. В любом случае - реализация XML-encoding - отстой в PHP. Нельзя задавать encoding в документе средствами XML. Поэтому надо ждать нормального релиза, а все что между - на вкус и цвет.

       Unknown 2003-05-16 23:09
    На самом деле iconv тоже не идеален - в частности на перекодировке из unicode в не-unicode если вдруг случилось так, что какой-то символ не может быть перекодирован - он может в некоторых случаях просто вернуть пустую строку, потеряв всю информацию

       2003-06-16 15:05
    всё это очень хорошо и очень правильно, за исключением одного: мы делаем реальные сайты. XML и XSLT существенно упрощают сайт, но не менее существенно сказываются на скорости его работы. то же относится и к PEAR. скрипт, в котором производится только загрузка моих dataobject'ов (класс PEAR) выполняется в течение 0.1 сек. целиком - выводящий таблицу, связанные select'ы и т.п. - в течение 1.9 сек. аналогичный скрипт на шаблонах и моих собственных библиотеках - 0.1 сек. не спорю, XML+XSLT - революционные инструменты. Если бы они только не были такими медленными...

     
     
     
        © 1997-2008 PHPClubTeam
    []