Как часто приходится работать с 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 — решать только вам.


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


Ссылки


Комментарии (10)


  1. Gemorroj
    01.12.2017 11:37

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


    1. BuPy7 Автор
      01.12.2017 11:42

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


  1. Vadiok
    01.12.2017 13:21

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


    Что-то типа этого
    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();


    1. ollisso
      01.12.2017 14:20
      +1

      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();
      


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


      1. Vadiok
        01.12.2017 14:24

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


  1. dmirogin
    01.12.2017 15:17

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


    1. Vadiok
      01.12.2017 16:00

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


      1. dmirogin
        01.12.2017 16:02

        но это лучше, чем код в начале

        Заголовок спойлера
        $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;
        


        1. Vadiok
          01.12.2017 16:06

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


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


  1. Gemorroj
    01.12.2017 16:13

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


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

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


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