Simple XML fluent writer and memory efficient XML reader.
- Fluent builder build over Document Object Model with automatic CDATA escaping, namespace support and other features
- Utilises XMLReader and Generator for memory efficient reading of large files
- The entire code is covered by unit tests
All the code snippets shown here are modified for clarity, so they may not be executable.
Writing Google Merchant XML feed file
/** @var Inspirum\XML\Builder\DocumentFactory $factory */ $locale = 'cs'; $currencyCode = 'CZK'; $xml = $factory->create(); $rss = $xml->addElement('rss', [ 'version' => '2.0', 'xmlns:g' => 'http://base.google.com/ns/1.0', ]); $channel = $rss->addElement('channel'); $channel->addTextElement('title', 'Google Merchant'); $channel->addTextElement('link', 'https://www.example.com'); $channel->addTextElement('description', 'Google Merchant products feed'); $channel->addTextElement('language', $locale); $channel->addTextElement('lastBuildDate', (new \DateTime())->format('D, d M y H:i:s O')); $channel->addTextElement('generator', 'Eshop'); foreach ($products as $product) { $item = $xml->createElement('item'); $item->addTextElement('g:id', $product->getId()); $item->addTextElement('title', $product->getName($locale)); $item->addTextElement('link', $product->getUrl()); $item->addTextElement('description', \strip_tags($product->getDescription($locale))); $item->addTextElement('g:image_link', $product->getImageUrl()); foreach ($product->getAdditionalImageUrls() as $imageUrl) { $item->addTextElement('g:additional_image_link', $imageUrl); } $price = $product->getPrice($currencyCode); $item->addTextElement('g:price', $price->getOriginalPriceWithVat() . ' ' . $currencyCode); if ($price->inDiscount()) { $item->addTextElement('g:sale_price', $price->getPriceWithVat() . ' ' . $currencyCode); } if ($product->hasEAN()) { $item->addTextElement('g:gtin', $product->getEAN()); } else { $item->addTextElement('g:identifier_exists', 'no'); } $item->addTextElement('g:condition', 'new'); if ($product->inStock()) { $item->addTextElement('g:availability', 'in stock'); } elseif ($product->hasPreorder()) { $item->addTextElement('g:availability', 'preorder'); $item->addTextElement('g:availability_date', $product->getDeliveryDate()); } else { $item->addTextElement('g:availability', 'out of stock'); } $item->addTextElement('g:brand', $product->getBrand()); $item->addTextElement('g:size', $product->getParameterValue('size', $locale)); $item->addTextElement('g:color', $product->getParameterValue('color', $locale)); $item->addTextElement('g:material', $product->getParameterValue('material', $locale)); if ($product->isVariant()) { $item->addTextElement('g:item_group_id', $product->getParentProductId()()); } if ($product->getCustomAttribute('google_category') !== null) { $item->addTextElement('g:google_product_category', $product->getCustomAttribute('google_category')); } elseif ($product->getMainCategory() !== null) { $item->addTextElement('g:product_type', $product->getMainCategory()->getFullname($locale)); } } $xml->validate('/google_feed.xsd'); $xml->save('/output/feeds/google.xml'); /** var_dump($xml->toString(true)); <?xml version="1.0" encoding="UTF-8"?> <rss version="2.0" xmlns:g="http://base.google.com/ns/1.0"> <channel> <title>Google Merchant</title> <link>https://www.example.com</link> <description>Google Merchant products feed</description> <language>cs</language> <lastBuildDate>Sat, 14 Nov 20 08:00:00 +0200</lastBuildDate> <generator>Eshop</generator> <item> <g:id>0001</g:id> <title><![CDATA[Sample products #1 A&B]]></title> <link>http://localhost/produkt/sample-product-1-a-b</link> <description>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</description> <g:image_link>http://localhost/images/no_image.webp</g:image_link> <g:price>19.99 CZK</g:price> <g:gtin>7220110003812</g:gtin> <g:condition>new</g:condition> <g:availability>in stock</g:availability> <g:brand>Co.</g:brand> </item> ... </channel> </rss> */Reading data from Google Merchant XML feed
/** @var \Inspirum\XML\Reader\ReaderFactory $factory */ $reader = $factory->create('/output/feeds/google.xml'); $title = $reader->nextNode('title')->getTextContent(); /** var_dump($title); 'Google Merchant' */ $lastBuildDate = $reader->nextNode('lastBuildDate')->getTextContent(); /** var_dump($lastBuildDate); '2020-08-25T13:53:38+00:00' */ $price = 0.0; foreach ($reader->iterateNode('item') as $item) { $data = $item->toArray(); $price += (float) $data['g:price']; } /** var_dump($price); 501.98 */Splitting data to XML fragments by xpath (with valid namespaces)
/** @var \Inspirum\XML\Reader\ReaderFactory $factory */ $reader = $factory->create('/output/feeds/google.xml'); foreach ($reader->iterateNode('/rss/channel/item', true) as $item) { $data = $item->toString(); $id = ($item->xpath('/item/g:id')[0] ?? null)?->getTextContent() // ... }Run composer require command
$ composer require inspirum/xmlor add requirement to your composer.json
"inspirum/xml": "^3.0"Available framework integrations:
But you can also use it without any framework implementation:
use Inspirum\XML\Builder\DefaultDocumentFactory; use Inspirum\XML\Builder\DefaultDOMDocumentFactory; use Inspirum\XML\Reader\DefaultReaderFactory; use Inspirum\XML\Reader\DefaultXMLReaderFactory; $documentFactory = new DefaultDocumentFactory(new DefaultDOMDocumentFactory()); $document = $documentFactory->create(); // ... $readerFactory = new DefaultReaderFactory(new DefaultXMLReaderFactory(), $documentFactory); $reader = $readerFactory->create('/path/to/file.xml'); // ...Optionally you can specify XML version and encoding (defaults to UTF-8).
use Inspirum\XML\Builder\DefaultDocumentFactory; $factory = new DefaultDocumentFactory() $xml = $factory->create('1.0', 'WINDOWS-1250'); /** <?xml version="1.0" encoding="WINDOWS-1250"?> */ $xml = $factory->create(); /** <?xml version="1.0" encoding="UTF-8"?> */Nesting elements
$a = $xml->addElement('a'); $a->addTextElement('b', 'BB', ['id' => 1]); $b = $a->addElement('b', ['id' => 2]); $b->addTextElement('c', 'CC'); /** <?xml version="1.0" encoding="UTF-8"?> <a> <b id="1">BB</a> <b id="2"> <c>CC</c> </b> </a> */Used as fluent builder
$xml->addElement('root')->addElement('a')->addElement('b', ['id' => 1])->addTextElement('c', 'CC'); /** <?xml version="1.0" encoding="UTF-8"?> <root> <a> <b id="2"> <c>CC</c> </b> </a> </root> */Automatic CDATA escaping
$a = $xml->addElement('a'); $a->addTextElement('b', 'me & you'); $a->addTextElement('b', '30 km'); /** <?xml version="1.0" encoding="UTF-8"?> <a> <b> <![CDATA[me & you]]> </b> <b> <![CDATA[30 km]]> </b> </a> */Forced CDATA escaping
$a = $xml->addElement('a'); $a->addTextElement('b', 'me'); $a->addTextElement('b', 'you', forcedEscape: true); /** <?xml version="1.0" encoding="UTF-8"?> <a> <b>me</b> <b> <![CDATA[you]]> </b> </a> */Adding XML fragments
$a = $xml->addElement('a'); $a->addXMLData('<b><c>CC</c></b><b>0</b>'); $a->addTextElement('b', '1'); /** <?xml version="1.0" encoding="UTF-8"?> <a> <b> <c>CC</c> </b> <b>0</b> <b>1</b> </a> */To use automatic namespace usage you only have to set xmlns:{prefix} attribute on (usually) root element.
Elements (or/and attributes) use given prefix as {prefix}:{localName}, and it will be created with createElementNS or createAttributeNS method.
$root = $xml->addElement('g:root', ['xmlns:g' =>'stock.xsd', 'g:version' => '2.0']); $items = $root->addElement('g:items'); $items->addTextElement('g:item', 1); $items->addTextElement('g:item', 2); $items->addTextElement('g:item', 3); /** <?xml version="1.0" encoding="UTF-8"?> <g:root xmlns:g="stock.xsd" g:version="2.0"> <g:items> <g:item>1</g:item> <g:item>2</g:item> <g:item>3</g:item> </a> </root> */Namespace support its necessary for XML validation with XSD schema
try { $xml->validate('/sample.xsd'); // valid XML } catch (\DOMException $exception) { // invalid XML }/sample.xml
<?xml version="1.0" encoding="utf-8"?> <g:feed xmlns:g="stock.xsd" g:version="2.0"> <g:updated>2020-08-25T13:53:38+00:00</g:updated> <title></title> <g:items> <g:item active="true" price="99.9"> <g:id>1</g:id> <g:name>Test 1</g:name> </g:item> <item active="true" price="19.9"> <g:id>2</g:id> <g:name>Test 2</g:name> </item> <g:item active="false" price="0"> <g:id>3</g:id> <g:name>Test 3</g:name> </g:item> </g:items> </g:feed>Reading XML files into Node instances
Read next node with given name
$node = $reader->nextNode('g:updated'); $node->getTextContent(); /** '2020-08-25T13:53:38+00:00' */ $node->toString(); /** <g:updated>2020-08-25T13:53:38+00:00</g:updated> */Powerful cast to array method
$data = $reader->nextNode('g:items')->toArray(); /** var_dump($ids); [ 'g:item' => [ 0 => [ 'g:id' => '1' 'g:name' => 'Test 1' '@attributes' => [ 'active' => 'true' 'price' => '99.9' ] ] 1 => [ 'g:id' => '3' 'g:name' => 'Test 3' '@attributes' => [ 'active' => 'false' 'price' => '0' ] ] ] 'item' => [ 0 => [ 'g:id' => '2' 'g:name' => 'Test 2' '@attributes' => [ 'active' => 'true' 'price' => '19.9' ] ] ] ] */Optional config supported for toArray method
use Inspirum\XML\Builder\DefaultDocumentFactory; use Inspirum\XML\Formatter\FullResponseConfig; $factory = new DefaultDocumentFactory() $config = new FullResponseConfig( attributesName: '@attr', valueName: '@val', autoCast: true, ); $data = $factory->createForFile('/sample.xml')->toArray($config); /** var_dump($ids); [ '@attr' => [] '@val' => null '@nodes' => [ 'g:feed' => [ 0 => [ '@attr' => [ 'g:version' => 2.0 ] '@val' => null '@nodes' => [ 'g:updated' => [ 0 => [ '@attr' => [] '@val' => '2020-08-25T13:53:38+00:00' '@nodes' => [] ] ] 'title' => [ 0 => [ '@attr' => [] '@val' => null '@nodes' => [] ] ] 'g:items' => [ 0 => [ '@attr' => [] '@val' => null '@nodes' => [ 'g:item' => [ 0 => [ '@attr' => [ 'active' => true 'price' => 99.9 ] '@val' => null '@nodes' => [ 'g:id' => [ 0 => [ '@attr' => [] '@val' => 1 '@nodes' => [] ] ] 'g:name' => [ 0 => [ '@attr' => [] '@val' => 'Test 1' '@nodes' => [] ] ] ] ] 1 => [ '@attr' => [ 'active' => false 'price' => 0 ] '@val' => null '@nodes' => [ 'g:id' => [ 0 => [ '@attr' => [] '@val' => 3 '@nodes' => [] ] ] 'g:name' => [ 0 => [ '@attr' => [] '@val' => 'Test 3' '@nodes' => [] ] ] ] ] ] 'item' => [ 0 => [ '@attr' => [ 'active' => true 'price' => 19.9 ] '@val' => null '@nodes' => [ 'g:id' => [ 0 => [ '@attr' => [] '@val' => 2 '@nodes' => [] ] ] 'g:name' => [ 0 => [ '@attr' => [] '@val' => 'Test 2' '@nodes' => [] ] ] ] ] ] ] ] ] ] ] ] ] ] */Iterate all nodes with given name
$ids = []; foreach ($reader->iterateNode('item') as $item) { $ids[] = $item->toArray()['id']; } /** var_dump($ids); [ 0 => '1' 1 => '3' ] */Splitting data to XML fragments (with valid namespaces)
$items = []; foreach ($reader->iterateNode('g:item', true) as $item) { $items[] = $item->toString(); } /** var_dump($items); [ 0 => '<g:item xmlns:g="stock.xsd" active="true" price="99.9"><g:id>1</g:id><g:name>Test 1</g:name></g:item>' 1 => '<g:item xmlns:g="stock.xsd" active="false" price="0"><g:id>3</g:id><g:name>Test 3</g:name></g:item>' ] */Inspirum\XML\Builder\DocumentFactoryInspirum\XML\Builder\DocumentInspirum\XML\Builder\NodeInspirum\XML\Reader\ReaderFactoryInspirum\XML\Reader\Reader
To run unit tests, run:
$ composer test:testTo show coverage, run:
$ composer test:coveragePlease see CONTRIBUTING and CODE_OF_CONDUCT for details.
If you discover any security related issues, please email tomas.novotny@inspirum.cz instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.