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 < 10 & tax > 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 | Сложная структура | Большие файлы, экспорт |