Как часто приходится работать с 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="<example danger="account"><WTF?!/></example>" Secure="81ad561784277fa864bf644d755fb164" OrderCount="1" CopyCount="4">
<Order DispathNumber="1033229706" Date="2016-09-25T12:45:10"><special>"chars"</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)
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();
ollisso
01.12.2017 14:20+1php.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();
Чукча не читатель, чукча писатель.
Vadiok
01.12.2017 14:24Точно, давно им пользовался, подзабыл ) Сейчас же в основном апи на JSON'ах делают.
dmirogin
01.12.2017 15:17Ну если как в начале, то это кошмар. Почему никто не читает документацию стандартной библиотеки?
php.net/manual/ru/book.xmlwriter.php
php.net/manual/ru/example.xmlwriter-oop.phpVadiok
01.12.2017 16:00В стандартной библиотеке все слишком избыточно. Ваша же 2-я ссылка с примером.
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;
Vadiok
01.12.2017 16:06По мне так оба варианта одной сложности, если известно, что исходные данные не будут содержать ломающих верстку символов.
Вот с SimpleXML, на который мне указали выше, все гораздо удобнее.
Конечно, XMLWriter, крут, но его, по-моему стоит использовать для чего-то большего, чем то, для чего указал автор поста:
Обычно потребность возникает при интеграции со сторонним сервисом, т.к. BetaPRO, OnTime или CDEK
Gemorroj
01.12.2017 16:13смотрю народ на xmlwriter ссылается, но он не покрывает всех юзкейсов.
бывает, что уже есть массив (не важно откуда получен), и из него нужно сделать xml.
делаю так:
- из массива создается dom объект (dom объект структурно идентичен исходному массиву)
- dom объект передается в XSLTProcessor::transformToXML
- в xslt преобразовании уже создаем xml нужной структуры
на больших xml так удобнее
вот утилиты типа описанной в статье помогают закрыть 1 пункт
Gemorroj
пользуюсь давно таким https://github.com/spatie/array-to-xml
BuPy7 Автор
Спасибо. В 2015ом не смог найти этот пакет (плохо искал значит), если честно, хоть и первый коммит у него от 17 марта 2015 года. Я свой первый коммит сделал 19 июля 2015 года.