Конструктор XML-строки из PHP-массива

Как часто приходится работать с XML PHP-разработчикам? Не так часто, на самом деле. Обычно потребность возникает при интеграции со сторонним сервисом, такие как BetaPRO, OnTime или CDEK. И вот тут обычно возникает такая ситуация, когда ваш код становится похожим на


$date = '2016-09-25T12:45:10';
$account = 'f62dcb094cc91617def72d9c260b4483';
$secure = '81ad561784277fa864bf644d755fb164';
$count = 1;
$copy = 4;
$dispatchNumber = '1033229706';
$orderDate = '2016-09-25T12:45:10';

$request = <<<XML
<?xml version="1.0" encoding="UTF-8"?> 
<OrdersPrint Date="{$date}" Account="{$account}" Secure="{$secure}" OrderCount="{$count}" CopyCount="{$copy}">
    <Order DispathNumber="{$orderNumber}" Date="{$orderDate}"/>
</OrdersPrint>
XML;

и это еще не все! Нужно позаботиться о том, чтобы значения атрибутов и содержимое, заключенное в теги, не содержало спецсимволов, присущие XML. Если для конкретно этого запроса можно быть уверенным, что ничего из спецсимволов сюда не попадет, то контролировать каждый запрос вовсе бы не хотелось. Поэтому через "фильтр" пропускается все. Отсюда следует, что нужно еще "загнаться" с htmlspecialchars или с CDATA, или с XMLWriter, и знать, как это применить и не раз еще "свернуть себе кровь". Как вы видите, времени стоит "убить" достаточно, а результат-то хочется уже сейчас. Эх… А как хотелось бы, чтобы XML можно было бы создавать так же быстро, как JSON: отдал массив, а тебе XML-строку, и никаких заморочек. Опечалившись сложившейся ситуацией я в далеком 2015ом году я решил сделать такой конструктор.


Вашему вниманию представляю xml-constructor для PHP начиная с версии 5.4 и до 7.2 на момент публикации данной статьи.


Использование


Для начала использования установим данный пакет через Composer:


$ composer require bupy7/xml-constructor

Его так же можно просто скопировать вручную куда вы хотите, т.к. пакет не имеет никаких доп. зависимостей, кроме как наличия libxml в самом PHP.


Теперь создадим XML-строку используя PHP-массив:


$date = '2016-09-25T12:45:10';
$account = 'f62dcb094cc91617def72d9c260b4483';
$secure = '81ad561784277fa864bf644d755fb164';
$count = 1;
$copy = 4;
$dispatchNumber = '1033229706';
$orderDate = '2016-09-25T12:45:10';

$in = [
    [
        'tag' => 'OrdersPrint',
        'attributes' => [
            'Date' => $date,
            'Account' => $account,
            'Secure' => $secure,
            'OrderCount' => $count,
            'CopyCount' => $copy,
        ],
        'elements' => [
            [
                'tag' => 'Order',
                'attributes' => [
                    'DispathNumber' => $dispatchNumber,
                    'Date' => $orderDate,
                ],
            ],
        ],
    ],
];
$request = (new \bupy7\xml\constructor\XmlConstructor())->fromArray($in)->toOutput();

Результат:


<?xml version="1.0" encoding="UTF-8"?>
<OrdersPrint Date="2016-09-25T12:45:10" Account="f62dcb094cc91617def72d9c260b4483" Secure="81ad561784277fa864bf644d755fb164" OrderCount="1" CopyCount="4">
    <Order DispathNumber="1033229706" Date="2016-09-25T12:45:10"/>
</OrdersPrint>

Вот и вся работа! Об остальном позаботится xml-constructor.


И давайте попробуем передать что-то "запрещенное" в значения и посмотрим, как будет вести себя xml-constructor:


$date = '2016-09-25T12:45:10';
$secure = '81ad561784277fa864bf644d755fb164';
$count = 1;
$copy = 4;
$dispatchNumber = '1033229706';
$orderDate = '2016-09-25T12:45:10';

// ACHTUNG !!!
$account = '<example danger="account"><WTF?!/></example>';
$orderContent = '<special>"chars"';

$in = [
    [
        'tag' => 'OrdersPrint',
        'attributes' => [
            'Date' => $date,
            'Account' => $account,
            'Secure' => $secure,
            'OrderCount' => $count,
            'CopyCount' => $copy,
        ],
        'elements' => [
            [
                'tag' => 'Order',
                'attributes' => [
                    'DispathNumber' => $dispatchNumber,
                    'Date' => $orderDate,
                ],
                'content' => $orderContent,
            ],
        ],
    ],   
];

$request = (new \bupy7\xml\constructor\XmlConstructor())->fromArray($in)->toOutput();

Результат:


<?xml version="1.0" encoding="UTF-8"?>
<OrdersPrint Date="2016-09-25T12:45:10" Account="&lt;example danger=&quot;account&quot;&gt;&lt;WTF?!/&gt;&lt;/example&gt;" Secure="81ad561784277fa864bf644d755fb164" OrderCount="1" CopyCount="4">
    <Order DispathNumber="1033229706" Date="2016-09-25T12:45:10">&lt;special&gt;&quot;chars&quot;</Order>
</OrdersPrint>

Требования


Создание XML-строки сводится к тому, что нужно передать PHP-массив с нужными ключами и в правильной структуре. Ключей всего четыре:


  • tag — строка с названием тега;
  • content — содержимое заключенное между тегом;
  • attributes — массив ключ-значение, где ключ — это, название атрибута (строка), а значение — его значение (строка);
  • elements — новая вложенность тегов внутри которого этот ключ был указан. Также будет содержать в себе все перечисленные выше элементы. Вложенность неограниченная.

Каждый элемент массива должен содержать массив с одним ключом tag, как минимум. Ключи attributes, content и elements необязательные.


Первый уровень вложенности есть ничто иное, как корни XML-документа, т.е.:


$in = [
    [
        'tag' => 'FirstRoot',
    ],
    [
        'tag' => 'SecondRoot',
        'content' => 'Content of SecondRoot',
    ],
];

$request = (new \bupy7\xml\constructor\XmlConstructor())->fromArray($in)->toOutput();

Результат:


<?xml version="1.0" encoding="UTF-8"?>
<FirstRoot/>
<SecondRoot>Content of SecondRoot</SecondRoot>

Конфигурация


Из конфигурации все только самое необходимое.


  • indentString — произвольная строка для отступов. По умолчанию 4 пробела. Если не хотите использовать отступы вообще — передайте false.
  • startDocument — массив ключ-значение с атрибутами XML декларации документа. По умолчанию это <?xml version="1.0" encoding="UTF-8"?>. Если вам не нужна декларация — передайте false.

Для применения конфигурации нужно передать массив ключ-значение в конструктор первым аргументом:


$date = '2016-09-25T12:45:10';
$account = 'f62dcb094cc91617def72d9c260b4483';
$secure = '81ad561784277fa864bf644d755fb164';
$count = 1;
$copy = 4;
$dispatchNumber = '1033229706';
$orderDate = '2016-09-25T12:45:10';

$in = [
    [
        'tag' => 'OrdersPrint',
        'attributes' => [
            'Date' => $date,
            'Account' => $account,
            'Secure' => $secure,
            'OrderCount' => $count,
            'CopyCount' => $copy,
        ],
        'elements' => [
            [
                'tag' => 'Order',
                'attributes' => [
                    'DispathNumber' => $dispatchNumber,
                    'Date' => $orderDate,
                ],
            ],
        ],
    ],
];
$request = (new \bupy7\xml\constructor\XmlConstructor([
        'indentString' => '****',
        'startDocument' => false,
    ]))
    ->fromArray($in)
    ->toOutput();

Результат:


<OrdersPrint Date="2016-09-25T12:45:10" Account="f62dcb094cc91617def72d9c260b4483" Secure="81ad561784277fa864bf644d755fb164" OrderCount="1" CopyCount="4"><Order DispathNumber="1033229706" Date="2016-09-25T12:45:10"/></OrdersPrint>

Заключение


Расширение очень простое и привносит массу удобств во время интеграции с сервисами использующими XML для своего API. Стоит ли использовать xml-constructor — решать только вам.


Спасибо за потраченное на прочтение время!


Ссылки


Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 12
  • +1

    пользуюсь давно таким https://github.com/spatie/array-to-xml

    • 0
      Спасибо. В 2015ом не смог найти этот пакет (плохо искал значит), если честно, хоть и первый коммит у него от 17 марта 2015 года. Я свой первый коммит сделал 19 июля 2015 года.
    • 0

      По-моему гораздо удобней было бы это делать через классы


      Что-то типа этого
      class Tag
      {
          protected $tag;
          protected $attributes;
          protected $childTags = [];
      
          public function __construct(string $tag, array $attributes = [])
          {
              $this->tag = $tag;
              $this->attributes = $attributes;
          }
      
          public function addChildTag(Tag $tag)
          {
              $this->childTags[] = $tag;
          }
      
          public function addAttribute(string $title, $value = null)
          {
              $this->attributes[$title] = $value;
              $this->childTags[] = $tag;
          }
      
          public function tagToXmlBlock()
          {
              $result = '<' . $this->tag . '%s>%s</' . $this->tag . '>';
              $attributeString = '';
              foreach ($this->attributes as $attribute => $value) {
                  $attributeString .= ' ' . $attribute . '="' . addslashes($value) . '"';
              }
              $innerTags = '';
              foreach ($this->childTags as $tag) {
                  $innerTags .= $tag->tagToXmlBlock();
              }
              sprintf($result, $attributeStringm, $innerTags);
              return $result;
          }
      
          public function getXmlDocument()
          {
              return '<?xml version="1.0" encoding="UTF-8"?>' . $this->tagToXmlBlock();
          }
      }
      
      $root = new Tag('root');
      $childTag = new Tag('child');
      $root->addChildTag($childTag);
      $root->addChildTag($childTag);
      echo $root->getXmlDocument();
      • +4
        php.net/manual/en/class.simplexmlelement.php
        php.net/manual/en/simplexmlelement.addchild.php

        $sxe = new SimpleXMLElement($xmlstr);
        $sxe->addAttribute('type', 'documentary');
        
        $movie = $sxe->addChild('movie');
        $movie->addChild('title', 'PHP2: More Parser Stories');
        $movie->addChild('plot', 'This is all about the people who make it work.');
        
        $characters = $movie->addChild('characters');
        $character  = $characters->addChild('character');
        $character->addChild('name', 'Mr. Parser');
        $character->addChild('actor', 'John Doe');
        
        $rating = $movie->addChild('rating', '5');
        $rating->addAttribute('type', 'stars');
         
        echo $sxe->asXML();
        


        Чукча не читатель, чукча писатель.

        • 0

          Точно, давно им пользовался, подзабыл ) Сейчас же в основном апи на JSON'ах делают.

      • +1
        Ну если как в начале, то это кошмар. Почему никто не читает документацию стандартной библиотеки?
        php.net/manual/ru/book.xmlwriter.php
        php.net/manual/ru/example.xmlwriter-oop.php
        • 0

          В стандартной библиотеке все слишком избыточно. Ваша же 2-я ссылка с примером.

          • 0
            но это лучше, чем код в начале
            Заголовок спойлера
            $request = <<<XML
            <?xml version="1.0" encoding="UTF-8"?> 
            <OrdersPrint Date="{$date}" Account="{$account}" Secure="{$secure}" OrderCount="{$count}" CopyCount="{$copy}">
                <Order DispathNumber="{$orderNumber}" Date="{$orderDate}"/>
            </OrdersPrint>
            XML;
            

            • 0

              По мне так оба варианта одной сложности, если известно, что исходные данные не будут содержать ломающих верстку символов.
              Вот с SimpleXML, на который мне указали выше, все гораздо удобнее.
              Конечно, XMLWriter, крут, но его, по-моему стоит использовать для чего-то большего, чем то, для чего указал автор поста:


              Обычно потребность возникает при интеграции со сторонним сервисом, т.к. BetaPRO, OnTime или CDEK
        • 0

          смотрю народ на xmlwriter ссылается, но он не покрывает всех юзкейсов.
          бывает, что уже есть массив (не важно откуда получен), и из него нужно сделать xml.
          делаю так:


          • из массива создается dom объект (dom объект структурно идентичен исходному массиву)
          • dom объект передается в XSLTProcessor::transformToXML
          • в xslt преобразовании уже создаем xml нужной структуры

          на больших xml так удобнее


          вот утилиты типа описанной в статье помогают закрыть 1 пункт

          • 0
            Нужно позаботиться о том, чтобы значения атрибутов и содержимое, заключенное в теги, не содержало спецсимволов, присущие XML

            Очень хотелось донести эту информацию тем людям, которые реализовывали большинство YML выгрузок в интернет магазинах. Постоянно невалидные данные приходилось исправлять.
            • 0
              посмотрите fluidxml github.com/servo-php/fluidxml

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.