简单的XML写入器和高效的XML读取器,具有强大的xml-to-array转换功能

v3.1.0 2023-11-05 11:56 UTC

This package is auto-updated.

Last update: 2024-09-17 13:22:03 UTC


README

Latest Stable Version Build Status Coverage Status Quality Score PHPStan Total Downloads Software License

简单的XML流畅写入器和高效的XML读取器。

  • 流畅构建器基于文档对象模型构建,具有自动CDATA转义、命名空间支持等特性
  • 利用XMLReader生成器,以高效读取大文件
  • 所有代码均由单元测试覆盖

使用示例

此处显示的所有代码片段均已修改以供清晰阅读,因此它们可能无法执行。

XML写入器

写入Google Merchant XML馈文件

/** @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>
*/

XML读取器

从Google Merchant XML馈读取数据

/** @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
*/

通过xpath(具有有效命名空间)分割数据到XML片段

/** @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()
    // ...
}

系统需求

安装

运行composer require命令

$ composer require inspirum/xml

或在您的composer.json中添加需求

"inspirum/xml": "^3.0"

使用

可用的框架集成

但您也可以在不使用任何框架实现的情况下使用它

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');
// ...

XML写入器

可选地,您可以为XML版本和编码(默认为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"?>
*/

嵌套元素

$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>
*/

作为流畅构建器使用

$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>
*/

自动CDATA转义

$a = $xml->addElement('a');
$a->addTextElement('b', 'me & you');
$a->addTextElement('b', '30&nbsp;km');

/**
<?xml version="1.0" encoding="UTF-8"?>
<a>
  <b>
     <![CDATA[me & you]]>
  </b>
  <b>
    <![CDATA[30&nbsp;km]]>
  </b>
</a>
*/

强制CDATA转义

$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>
*/

添加XML片段

$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>
*/

要使用自动命名空间,您只需在(通常是)根元素上设置xmlns:{prefix}属性。

元素(或/和属性)使用给定的前缀作为{prefix}:{localName},它将使用createElementNScreateAttributeNS方法创建。

$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>
*/

命名空间支持对于使用XSD模式进行XML验证是必要的

try {
    $xml->validate('/sample.xsd');
    // valid XML
} catch (\DOMException $exception) {
    // invalid XML
}

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>

将XML文件读取到Node实例中

使用给定名称读取下一个节点

$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>
*/

强大的转换为数组的方法

$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'
      ]
    ]
  ]
]
*/

支持为toArray方法提供可选配置

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' => []
                        ]
                      ]
                    ]
                  ]
                ]
              ]
            ]
          ]
        ]
      ]
    ]
  ]
]
*/

迭代给定名称的所有节点

$ids = [];
foreach ($reader->iterateNode('item') as $item) {
    $ids[] = $item->toArray()['id'];
}

/**
var_dump($ids);
[
  0 => '1'
  1 => '3'
]
*/

通过xpath(具有有效命名空间)分割数据到XML片段

$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>'
]
*/

所有可用方法

测试

要运行单元测试,请运行

$ composer test:test

要显示覆盖率,请运行

$ composer test:coverage

贡献

有关详细信息,请参阅CONTRIBUTINGCODE_OF_CONDUCT

安全性

如果您发现任何与安全性相关的问题,请通过电子邮件tomas.novotny@inspirum.cz而不是使用问题跟踪器来报告。

鸣谢

许可

MIT许可(MIT)。有关更多信息,请参阅许可文件