Всем привет. Хочу поделиться своим опытом в парсинге XML, хочу рассказать об инструменте который мне в этом помогает.
XML ещё жив и иногда его приходиться парсить. Особенно если вы работаете со СМЭВ (привет всем ребятам для которых "ФОИВ" не пустой звук :) ).
Цели у такого парсинга могут быть самые разные, от банального ответа на вопрос какое пространство имён используется в xml-документе, до необходимости получить структурированное представление для документа вцелом.
Инструмент для каждой цели будет свой. Пространство имён можно найти поиском подстроки или регулярным выражением. Что бы сделать из xml-документа структурированное представление (DTO) - придётся писать парсер.
Для работы с XML в PHP есть пара встроенных классов. Это XMLReader и SimpleXMLElement.
XMLReader
С помощью XMLReader парсинг будет выглядеть примерно так :
$reader = (new XMLReader());
$reader->XML($content);
while ($reader->read()) {
$this->parse($reader);
}
Внутри метода parse(XMLReader $xml) будут бесконечные:
$name = $xml->name;
$value = $xml->expand()->textContent;
$attrVal = $xml->getAttribute('attribute');
$isElem = $xml->nodeType === XMLReader::ELEMENT;
Для небольших документов или когда нам из всего документа надо только пару элементов, это приемлемо, на больших объёмах - начинает в глазах рябить от однообразного кода, плюс совесть грызёт за оверхэд от перебора всех элементов документа.
SimpleXMLElement
Провести анализ только нужных элементов помогает SimpleXMLElement. Этот класс из XML-документа делает объект, у которого все элементы и атрибуты становятся свойствами, то есть появляется возможность работать только с определёнными элементами, а не со всеми подряд, пример:
$document = new SimpleXMLElement($content);
/* имя корневого элемента */
$name = $document->getName();
/* получить произвольный элемент */
$primary = $document
->Message
->ResponseContent
->content
->MessagePrimaryContent ?? null;
/* получить элементы определённого пространства имён */
$attachment = $primary
->children(
'urn://x-artefacts-fns-zpvipegr/root/750-08/4.0.1'
)
->xpath('tns:Вложения/fnst:Вложение')[0];
/* получить значение элемента */
$fileName = $attachment
->xpath('//fnst:ИмяФайла')[0]
->__toString();
Удобно, да не совсем. Если имя элемента на кириллице, то обратиться к нему через свойство не получиться, придётся использовать SimpleXMLElement::xpath(). С множественными значениями так же приходиться работать через SimpleXMLElement::xpath(). Кроме того SimpleXMLElement имеет свои особенности и некоторые вещи далеко не очевидны.
Converter
Есть способ проще. Достаточно XML-документ привести к массиву. В работе с массивами нет ни каких подводных камней. Массив из XML делается в пару строчек кода:
$xml=<<<XML
<b attr4="55">
<c>ccc
<d/>
</c>
0000
</b>
XML;
$fabric = (new NavigatorFabric())->setXml($xml);
$converter = $fabric->makeConverter();
$arrayRepresentationOfXml = $converter->toArray();
Каждый XML-элемент будет представлен массивом, состоящим в свою очередь, из трёх других массивов.
Соответственно:
массив с индексом '*value' содержит значение элемента,
'*attributes' - атрибуты элемента,
'*elements' - вложенные элементы.
/*
'b' =>
array (
'*value' => '0000',
'*attributes' =>
array (
'attr4' => '55',
),
'*elements' =>
array (
'c' =>
array (
),
),
),
*/
Если элемент множественный, то есть встречается в документе несколько раз подряд, то все его вхождения будут в массиве с индексом '*multiple'.
$xml=<<<XML
<doc>
<qwe>first occurrence</qwe>
<qwe>second occurrence</qwe>
</doc>
XML;
/*
'doc' =>
array (
'qwe' =>
array (
'*multiple' =>
array (
0 =>
array (
'*value' => 'first occurrence',
),
1 =>
array (
'*value' => 'second occurrence',
)
)
)
)
*/
Но и это ещё не всё.
XmlNavigator
Если от работы с XML-документов как с массивом, у вас в глазах рябит от квадратных скобочек, то XmlNavigator - это ваш вариант, создаётся так же в две строки кода.
/* документ */
$xml = <<<XML
<doc attrib="a" option="o" >666
<base/>
<valuable>element value</valuable>
<complex>
<a empty=""/>
<b val="x"/>
<b val="y"/>
<b val="z"/>
<c>0</c>
<c v="o"/>
<c/>
<different/>
</complex>
</doc>
XML;
$fabric = (new NavigatorFabric())->setXml($xml);
$navigator = $fabric->makeNavigator();
XmlNavigator делает, то же самое что и Converter, но предоставляет API, и с документом мы работаем как с объёктом.
Имя элемента, метод name()
/* Имя элемента */
echo $navigator->name();
/* doc */
Значение элемента, метод value()
/* Значение элемента */
echo $navigator->value();
/* 666 */
Список атрибутов, метод attribs()
/* get list of attributes */
echo var_export($navigator->attribs(), true);
/*
array (
0 => 'attrib',
1 => 'option',
)
*/
Значение атрибута, метод get()
/* get attribute value */
echo $navigator->get('attrib');
/* a */
Список вложенных элементов, метод elements()
/* Список вложенных элементов */
echo var_export($navigator->elements(), true);
/*
array (
0 => 'base',
1 => 'valuable',
2 => 'complex',
)
*/
Получить вложенный элемент, метод pull()
/* Получить вложенный элемент */
$nested = $navigator->pull('complex');
echo $nested->name();
/* complex */
echo var_export($nested->elements(), true);
/*
array (
0 => 'a',
1 => 'different',
2 => 'b',
3 => 'c',
)
*/
Перебрать все вхождения множественного элемента, метод next()
/* Получить вложенный элемент вложенного элемента */
$multiple = $navigator->pull('complex')->pull('b');
/* Перебрать все вхождения множественного элемента */
foreach ($multiple->next() as $index => $instance) {
echo " {$instance->name()}[$index]" .
" => {$instance->get('val')};";
}
/*
b[0] => x; b[1] => y; b[2] => z;
*/
Все методы класса XmlNavigator
Класс XmlNavigator реализует интерфейс IXmlNavigator.
<?php
namespace SbWereWolf\XmlNavigator;
interface IXmlNavigator
{
public function name(): string;
public function hasValue(): string;
public function value(): string;
public function hasAttribs(): bool;
public function attribs(): array;
public function get(string $name = null): string;
public function hasElements(): bool;
public function elements(): array;
public function pull(string $name): IXmlNavigator;
public function isMultiple(): bool;
public function next();
}
Из названий методов очевидно их назначение. Не очевидные были рассмотрены выше.
Как установить?
composer require sbwerewolf/xml-navigator
Заключение
В работе приходиться использовать сначала SimpleXMLElement - с его помощью из всего документа получаем необходимый элемент, и уже с этим элементом работаем через XmlNavigator.
$document = new SimpleXMLElement($content);
$primary = $document
->Message
->ResponseContent
->content
->MessagePrimaryContent;
$attachment = $primary
->children(
'urn://x-artefacts-fns-zpvipegr/root/750-08/4.0.1'
)
->xpath('tns:Вложения')[0];
$fabric = (new NavigatorFabric())->setSimpleXmlElement($attachment);
$navigator = $fabric->makeNavigator();
Желаю вам приятного использования.
Эпилог
Конечно у вас могут быть свои альтернативы для работы с XML. Предлагаю поделиться в комментариях.
Конечно, не могу сказать, что XmlNavigator поможет с любым XML - не проверял, но с обычными документами, без хитростей в схеме документа, проблем не было.
Если вам важен порядок следования элементов, то придётся пользоваться XMLReader. Потому что SimpleXMLElement приводит документ к объекту, а у объекта нет такого понятия как порядок следования элементов.