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

от автора

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Чем вы парсите XML?
10.64% XML, что это? 5
29.79% XMLReader 14
48.94% SimpleXMLElement 23
8.51% Сторонней библиотекой (пожалуйста напишите название в комментариях) 4
12.77% можно попробовать эту либу 6
Проголосовали 47 пользователей. Воздержались 10 пользователей.

ссылка на оригинал статьи https://habr.com/ru/post/599067/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *