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/xml
or 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\DocumentFactory
Inspirum\XML\Builder\Document
Inspirum\XML\Builder\Node
Inspirum\XML\Reader\ReaderFactory
Inspirum\XML\Reader\Reader
To run unit tests, run:
$ composer test:test
To show coverage, run:
$ composer test:coverage
Please see CONTRIBUTING and CODE_OF_CONDUCT for details.
If you discover any security related issues, please email [email protected] instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.