MidТеория12 min

Генерация XML

XMLWriter, DOM создание, SimpleXML генерация, XSLT трансформации

PHP предоставляет несколько способов создания XML: SimpleXML (быстрый и простой), DOM (полный контроль), XMLWriter (потоковый, эффективный по памяти) и XSLT (трансформация существующего XML). Выбор зависит от объема данных и требований к форматированию.

SimpleXML -- быстрое создание

<?php
declare(strict_types=1);

// === Create XML from scratch ===
$xml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><catalog/>');

// Add child elements
$book = $xml->addChild('book');
$book->addAttribute('id', '1');
$book->addAttribute('lang', 'en');

$book->addChild('title', 'PHP 8 in Action');
$book->addChild('author', 'John Doe');
$book->addChild('price', '39.99')->addAttribute('currency', 'USD');

// Nested elements
$tags = $book->addChild('tags');
$tags->addChild('tag', 'php');
$tags->addChild('tag', 'programming');

// Add second book
$book2 = $xml->addChild('book');
$book2->addAttribute('id', '2');
$book2->addChild('title', 'Symfony Deep Dive');
$book2->addChild('author', 'Jane Smith');
$book2->addChild('price', '44.99')->addAttribute('currency', 'EUR');

// Output as string
$xmlString = $xml->asXML();
echo $xmlString;

// Save to file
$xml->asXML('/path/to/catalog.xml');

// ⚠️ asXML() output is NOT formatted (no indentation)
// For pretty output, use DOM

// Formatted output via DOM
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadXML($xml->asXML());
echo $dom->saveXML();

SimpleXML -- работа со спецсимволами

<?php
declare(strict_types=1);

// SimpleXML automatically escapes special characters in addChild()
$xml = new SimpleXMLElement('<root/>');

// Special characters are escaped automatically
$item = $xml->addChild('description', 'Price: 5 < 10 & tax > 0');
echo $xml->asXML();
// <description>Price: 5 &lt; 10 &amp; tax &gt; 0</description>

// ⚠️ But addChild does NOT escape in element names!
// Element names must be valid XML names

// For CDATA sections, use DOM instead
$dom = dom_import_simplexml($xml);
$cdata = $dom->ownerDocument->createCDATASection('<script>alert("XSS")</script>');
$dom->appendChild($cdata);

// Adding namespaced elements
$xml = new SimpleXMLElement('<root xmlns:dc="http://purl.org/dc/elements/1.1/"/>');
$xml->addChild('dc:creator', 'John', 'http://purl.org/dc/elements/1.1/');
$xml->addChild('dc:date', '2024-01-15', 'http://purl.org/dc/elements/1.1/');

DOM -- полный контроль над генерацией

<?php
declare(strict_types=1);

// === Create XML document ===
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;  // Pretty print with indentation

// Create root element
$catalog = $dom->createElement('catalog');
$dom->appendChild($catalog);

// Create book element with attributes
$book = $dom->createElement('book');
$book->setAttribute('id', '1');
$book->setAttribute('lang', 'en');
$catalog->appendChild($book);

// Add child elements with text content
$title = $dom->createElement('title', 'PHP 8 in Action');
$book->appendChild($title);

$author = $dom->createElement('author', 'John Doe');
$book->appendChild($author);

// Element with attribute
$price = $dom->createElement('price', '39.99');
$price->setAttribute('currency', 'USD');
$book->appendChild($price);

// Nested structure
$tags = $dom->createElement('tags');
$book->appendChild($tags);

$tag1 = $dom->createElement('tag', 'php');
$tag2 = $dom->createElement('tag', 'programming');
$tags->appendChild($tag1);
$tags->appendChild($tag2);

// Output
echo $dom->saveXML();

// Save to file
$dom->save('/path/to/output.xml');

// Output without XML declaration
echo $dom->saveXML($dom->documentElement);

DOM -- продвинутые возможности

<?php
declare(strict_types=1);

$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;

$root = $dom->createElement('document');
$dom->appendChild($root);

// === Text nodes ===
$paragraph = $dom->createElement('p');
$text = $dom->createTextNode('This is ');
$paragraph->appendChild($text);

// Bold text inside paragraph
$bold = $dom->createElement('b', 'important');
$paragraph->appendChild($bold);

$text2 = $dom->createTextNode(' content.');
$paragraph->appendChild($text2);
$root->appendChild($paragraph);
// Result: <p>This is <b>important</b> content.</p>

// === CDATA section ===
$script = $dom->createElement('code');
$cdata = $dom->createCDATASection('if (a < b && c > d) { return true; }');
$script->appendChild($cdata);
$root->appendChild($script);
// Result: <code><![CDATA[if (a < b && c > d) { return true; }]]></code>

// === Comments ===
$comment = $dom->createComment(' Generated by PHP at ' . date('Y-m-d H:i:s') . ' ');
$root->insertBefore($comment, $root->firstChild);

// === Processing instructions ===
$pi = $dom->createProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="style.xsl"');
$dom->insertBefore($pi, $dom->documentElement);

// === Attributes via createAttribute() ===
$element = $dom->createElement('item');
$attr = $dom->createAttribute('id');
$attr->value = '42';
$element->appendChild($attr);
$root->appendChild($element);

// === Fragments — build reusable subtrees ===
$fragment = $dom->createDocumentFragment();
$fragment->appendXML('<name>John</name><email>[email protected]</email>');
$element->appendChild($fragment);

// === Clone nodes ===
$clone = $element->cloneNode(deep: true); // true = clone children too
$clone->setAttribute('id', '43');
$root->appendChild($clone);

// === Import nodes from another document ===
$otherDom = new DOMDocument();
$otherDom->loadXML('<data><value>imported</value></data>');
$imported = $dom->importNode($otherDom->documentElement, deep: true);
$root->appendChild($imported);

echo $dom->saveXML();

DOM -- пространства имен

<?php
declare(strict_types=1);

$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;

// Create root with namespace
$root = $dom->createElementNS('http://www.w3.org/2005/Atom', 'feed');
$dom->appendChild($root);

// Add namespace declaration for Dublin Core
$root->setAttributeNS(
    'http://www.w3.org/2000/xmlns/',
    'xmlns:dc',
    'http://purl.org/dc/elements/1.1/',
);

// Add elements in default namespace
$title = $dom->createElement('title', 'My Blog Feed');
$root->appendChild($title);

$link = $dom->createElement('link');
$link->setAttribute('href', 'https://example.com');
$link->setAttribute('rel', 'alternate');
$root->appendChild($link);

// Add entry
$entry = $dom->createElement('entry');
$root->appendChild($entry);

$entryTitle = $dom->createElement('title', 'First Post');
$entry->appendChild($entryTitle);

// Add namespaced elements
$creator = $dom->createElementNS(
    'http://purl.org/dc/elements/1.1/',
    'dc:creator',
    'John Doe',
);
$entry->appendChild($creator);

echo $dom->saveXML();
/*
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <title>My Blog Feed</title>
  <link href="https://example.com" rel="alternate"/>
  <entry>
    <title>First Post</title>
    <dc:creator>John Doe</dc:creator>
  </entry>
</feed>
*/

XMLWriter -- потоковая генерация

XMLWriter идеален для создания больших XML файлов, так как не хранит весь документ в памяти.

<?php
declare(strict_types=1);

// === Write to memory ===
$writer = new XMLWriter();
$writer->openMemory();

$writer->startDocument('1.0', 'UTF-8');
$writer->setIndent(true);
$writer->setIndentString('    '); // 4 spaces

// Root element
$writer->startElement('catalog');

// First book
$writer->startElement('book');
$writer->writeAttribute('id', '1');
$writer->writeAttribute('lang', 'en');

$writer->writeElement('title', 'PHP 8 in Action');
$writer->writeElement('author', 'John Doe');

// Element with attributes
$writer->startElement('price');
$writer->writeAttribute('currency', 'USD');
$writer->text('39.99');
$writer->endElement(); // price

// Nested tags
$writer->startElement('tags');
$writer->writeElement('tag', 'php');
$writer->writeElement('tag', 'programming');
$writer->endElement(); // tags

$writer->endElement(); // book

// Second book (shorthand methods)
$writer->startElement('book');
$writer->writeAttribute('id', '2');
$writer->writeElement('title', 'Symfony Guide');
$writer->writeElement('author', 'Jane Smith');
$writer->endElement(); // book

$writer->endElement(); // catalog
$writer->endDocument();

// Get the XML string
$xmlString = $writer->outputMemory();
echo $xmlString;

// === Write directly to file ===
$writer = new XMLWriter();
$writer->openURI('/path/to/output.xml');
$writer->startDocument('1.0', 'UTF-8');
$writer->setIndent(true);

$writer->startElement('data');
// ... write elements ...
$writer->endElement();

$writer->endDocument();
$writer->flush(); // Write buffer to file

XMLWriter -- продвинутые возможности

<?php
declare(strict_types=1);

$writer = new XMLWriter();
$writer->openMemory();
$writer->startDocument('1.0', 'UTF-8');
$writer->setIndent(true);

// === CDATA sections ===
$writer->startElement('description');
$writer->writeCdata('Content with <special> & "characters"');
$writer->endElement();
// Result: <description><![CDATA[Content with <special> & "characters"]]></description>

// === Comments ===
$writer->writeComment(' This is a comment ');
// Result: <!-- This is a comment -->

// === Processing instructions ===
$writer->writePI('xml-stylesheet', 'type="text/xsl" href="style.xsl"');

// === Namespaces ===
$writer->startElementNS('atom', 'feed', 'http://www.w3.org/2005/Atom');
$writer->writeElementNS('atom', 'title', null, 'My Feed');
$writer->writeElementNS('dc', 'creator', 'http://purl.org/dc/elements/1.1/', 'Author');
$writer->endElement();

// === DTD ===
$writer->writeDtd('catalog', null, 'catalog.dtd');

// === Raw XML ===
$writer->writeRaw('<already-formatted>content</already-formatted>');

$writer->endDocument();
echo $writer->outputMemory();

XMLWriter -- генерация больших файлов

<?php
declare(strict_types=1);

// Generating large XML efficiently — flush to file periodically
function generateLargeXml(string $outputPath, PDO $db): void
{
    $writer = new XMLWriter();
    $writer->openURI($outputPath);
    $writer->startDocument('1.0', 'UTF-8');
    $writer->setIndent(true);

    $writer->startElement('products');

    // Stream from database
    $stmt = $db->query('SELECT id, name, price, description FROM products');

    $count = 0;
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        $writer->startElement('product');
        $writer->writeAttribute('id', (string) $row['id']);
        $writer->writeElement('name', $row['name']);
        $writer->writeElement('price', number_format((float) $row['price'], 2, '.', ''));

        $writer->startElement('description');
        $writer->writeCdata($row['description']);
        $writer->endElement();

        $writer->endElement(); // product

        $count++;

        // Flush to file every 1000 products (free memory)
        if ($count % 1000 === 0) {
            $writer->flush();
        }
    }

    $writer->endElement(); // products
    $writer->endDocument();
    $writer->flush();

    echo "Generated $count products to $outputPath\n";
}

XSLT -- трансформация XML

<?php
declare(strict_types=1);

// XSLT transforms XML documents using XSL stylesheets
// Requires ext-xsl

$xmlString = <<<XML
<?xml version="1.0"?>
<catalog>
    <book>
        <title>PHP 8 in Action</title>
        <author>John Doe</author>
        <price>39.99</price>
    </book>
    <book>
        <title>Symfony Guide</title>
        <author>Jane Smith</author>
        <price>44.99</price>
    </book>
</catalog>
XML;

$xslString = <<<XSL
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="html" indent="yes"/>

    <xsl:template match="/catalog">
        <html>
            <body>
                <h1>Book Catalog</h1>
                <table border="1">
                    <tr>
                        <th>Title</th>
                        <th>Author</th>
                        <th>Price</th>
                    </tr>
                    <xsl:for-each select="book">
                        <xsl:sort select="title"/>
                        <tr>
                            <td><xsl:value-of select="title"/></td>
                            <td><xsl:value-of select="author"/></td>
                            <td>$<xsl:value-of select="price"/></td>
                        </tr>
                    </xsl:for-each>
                </table>
                <p>Total books: <xsl:value-of select="count(book)"/></p>
            </body>
        </html>
    </xsl:template>
</xsl:stylesheet>
XSL;

// Load XML and XSL
$xmlDoc = new DOMDocument();
$xmlDoc->loadXML($xmlString);

$xslDoc = new DOMDocument();
$xslDoc->loadXML($xslString);

// Create processor and import stylesheet
$processor = new XSLTProcessor();
$processor->importStyleSheet($xslDoc);

// Transform to string
$html = $processor->transformToXml($xmlDoc);
echo $html;

// Transform to DOMDocument
$resultDoc = $processor->transformToDoc($xmlDoc);
echo $resultDoc->saveHTML();

// Transform and save to file
$processor->transformToURI($xmlDoc, '/path/to/output.html');

// Pass PHP variables to XSLT
$processor->setParameter('', 'currentDate', date('Y-m-d'));
$processor->setParameter('', 'userName', 'John');
// In XSLT: <xsl:value-of select="$currentDate"/>

// Register PHP functions for use in XSLT (use with caution!)
$processor->registerPHPFunctions(['strtoupper', 'date']);
// In XSLT: <xsl:value-of select="php:function('strtoupper', string(title))"/>

Практический пример: XML Sitemap генератор

<?php
declare(strict_types=1);

final class SitemapGenerator
{
    private XMLWriter $writer;
    private int $urlCount = 0;
    private const MAX_URLS = 50_000;
    private const SITEMAP_NS = 'http://www.sitemaps.org/schemas/sitemap/0.9';

    public function __construct(
        private readonly string $outputPath,
    ) {
        $this->writer = new XMLWriter();
        $this->writer->openURI($this->outputPath);
        $this->writer->startDocument('1.0', 'UTF-8');
        $this->writer->setIndent(true);
        $this->writer->startElementNS(null, 'urlset', self::SITEMAP_NS);
    }

    public function addUrl(
        string $loc,
        ?DateTimeInterface $lastmod = null,
        string $changefreq = 'weekly',
        float $priority = 0.5,
    ): void {
        if ($this->urlCount >= self::MAX_URLS) {
            throw new OverflowException('Sitemap URL limit (50,000) reached');
        }

        $this->writer->startElement('url');
        $this->writer->writeElement('loc', $loc);

        if ($lastmod !== null) {
            $this->writer->writeElement('lastmod', $lastmod->format('Y-m-d'));
        }

        $this->writer->writeElement('changefreq', $changefreq);
        $this->writer->writeElement('priority', number_format($priority, 1));
        $this->writer->endElement();

        $this->urlCount++;

        // Flush every 1000 URLs
        if ($this->urlCount % 1000 === 0) {
            $this->writer->flush();
        }
    }

    public function finish(): int
    {
        $this->writer->endElement(); // urlset
        $this->writer->endDocument();
        $this->writer->flush();

        return $this->urlCount;
    }
}

// Usage
$sitemap = new SitemapGenerator('/var/www/public/sitemap.xml');

$sitemap->addUrl('https://example.com/', new DateTimeImmutable(), 'daily', 1.0);
$sitemap->addUrl('https://example.com/about', new DateTimeImmutable('-1 week'), 'monthly', 0.8);
$sitemap->addUrl('https://example.com/products', new DateTimeImmutable(), 'daily', 0.9);

// Add from database
// foreach ($products as $product) {
//     $sitemap->addUrl(
//         "https://example.com/products/{$product['slug']}",
//         new DateTimeImmutable($product['updated_at']),
//         'weekly',
//         0.7,
//     );
// }

$count = $sitemap->finish();
echo "Sitemap generated with $count URLs\n";

Практический пример: RSS Feed генератор

<?php
declare(strict_types=1);

final class RssFeedGenerator
{
    private DOMDocument $dom;
    private DOMElement $channel;

    public function __construct(
        string $title,
        string $link,
        string $description,
        string $language = 'en',
    ) {
        $this->dom = new DOMDocument('1.0', 'UTF-8');
        $this->dom->formatOutput = true;

        $rss = $this->dom->createElement('rss');
        $rss->setAttribute('version', '2.0');
        $rss->setAttributeNS(
            'http://www.w3.org/2000/xmlns/',
            'xmlns:atom',
            'http://www.w3.org/2005/Atom',
        );
        $this->dom->appendChild($rss);

        $this->channel = $this->dom->createElement('channel');
        $rss->appendChild($this->channel);

        $this->channel->appendChild($this->dom->createElement('title', $title));
        $this->channel->appendChild($this->dom->createElement('link', $link));
        $this->channel->appendChild($this->dom->createElement('description', $description));
        $this->channel->appendChild($this->dom->createElement('language', $language));
        $this->channel->appendChild(
            $this->dom->createElement('lastBuildDate', date(DATE_RSS))
        );
    }

    public function addItem(
        string $title,
        string $link,
        string $description,
        DateTimeInterface $pubDate,
        string $author = '',
        string $guid = '',
    ): self {
        $item = $this->dom->createElement('item');

        $item->appendChild($this->dom->createElement('title', $title));
        $item->appendChild($this->dom->createElement('link', $link));

        // Description with CDATA
        $descElement = $this->dom->createElement('description');
        $descElement->appendChild($this->dom->createCDATASection($description));
        $item->appendChild($descElement);

        $item->appendChild(
            $this->dom->createElement('pubDate', $pubDate->format(DATE_RSS))
        );

        if ($author !== '') {
            $item->appendChild($this->dom->createElement('author', $author));
        }

        $guidElement = $this->dom->createElement('guid', $guid ?: $link);
        $guidElement->setAttribute('isPermaLink', $guid === '' ? 'true' : 'false');
        $item->appendChild($guidElement);

        $this->channel->appendChild($item);

        return $this;
    }

    public function toXml(): string
    {
        return $this->dom->saveXML();
    }

    public function save(string $path): void
    {
        $this->dom->save($path);
    }
}

// Usage
$feed = new RssFeedGenerator(
    title: 'My Blog',
    link: 'https://example.com',
    description: 'Latest articles from My Blog',
);

$feed->addItem(
    title: 'Getting Started with PHP 8.4',
    link: 'https://example.com/php-84-guide',
    description: '<p>A comprehensive guide to new features in PHP 8.4</p>',
    pubDate: new DateTimeImmutable('2024-11-21'),
    author: '[email protected] (John Doe)',
);

$feed->addItem(
    title: 'Symfony 7 Best Practices',
    link: 'https://example.com/symfony-7-practices',
    description: '<p>Learn the best practices for Symfony 7 development</p>',
    pubDate: new DateTimeImmutable('2024-11-15'),
);

echo $feed->toXml();
$feed->save('/var/www/public/feed.xml');

Практический пример: экспорт данных в XML

<?php
declare(strict_types=1);

function exportToXml(array $data, string $rootElement = 'data', string $itemElement = 'item'): string
{
    $writer = new XMLWriter();
    $writer->openMemory();
    $writer->startDocument('1.0', 'UTF-8');
    $writer->setIndent(true);

    $writer->startElement($rootElement);

    foreach ($data as $item) {
        $writer->startElement($itemElement);
        writeArrayToXml($writer, $item);
        $writer->endElement();
    }

    $writer->endElement();
    $writer->endDocument();

    return $writer->outputMemory();
}

function writeArrayToXml(XMLWriter $writer, array $data): void
{
    foreach ($data as $key => $value) {
        if (is_int($key)) {
            $key = 'item_' . $key;
        }

        if (is_array($value)) {
            $writer->startElement($key);
            writeArrayToXml($writer, $value);
            $writer->endElement();
        } elseif (is_bool($value)) {
            $writer->writeElement($key, $value ? 'true' : 'false');
        } elseif ($value === null) {
            $writer->startElement($key);
            $writer->writeAttribute('xsi:nil', 'true');
            $writer->endElement();
        } else {
            // Check if value needs CDATA
            $strValue = (string) $value;
            if (preg_match('/[<>&]/', $strValue)) {
                $writer->startElement($key);
                $writer->writeCdata($strValue);
                $writer->endElement();
            } else {
                $writer->writeElement($key, $strValue);
            }
        }
    }
}

// Usage
$users = [
    ['id' => 1, 'name' => 'John Doe', 'email' => '[email protected]', 'roles' => ['admin', 'editor']],
    ['id' => 2, 'name' => 'Jane & Bob', 'email' => '[email protected]', 'roles' => ['user']],
];

$xml = exportToXml($users, 'users', 'user');
echo $xml;

Сравнение: SimpleXML vs DOM vs XMLWriter

Критерий SimpleXML DOM XMLWriter
Простота Просто Средне Просто
Память Весь документ Весь документ Потоковый
Большие файлы Нет Нет Да
Форматирование Нет (нужен DOM) formatOutput setIndent
Namespaces Ограниченно Полная поддержка Полная поддержка
CDATA Через DOM Да Да
Комментарии Нет Да Да
Модификация Ограниченно Полная Только запись
Когда Простой XML, API Сложная структура Большие файлы, экспорт

Тесты

Проверь себя

5 из 11

Как создать элемент с пространством имен в XMLWriter?

Что делает XMLWriter::flush()?

Какой максимальный размер XML Sitemap по стандарту?

Как добавить CDATA секцию через SimpleXML?

Как добавить атрибут к элементу в SimpleXML?