Работаем с XML как с массивом, на PHP

Kate

Administrator
Команда форума
Всем привет. Хочу поделиться своим опытом в парсинге 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 приводит документ к объекту, а у объекта нет такого понятия как порядок следования элементов.

 
Сверху