XML #

XML (eXtensible Markup Language) is still very relevant in the modern PHP world — integration with enterprise systems (SAP, Oracle), SOAP web services, RSS/Atom feeds, configuration (Symfony, Maven, Ant), and various data exchange formats in the finance, government, and logistics industries still use XML extensively. PHP provides three different ways to work with XML: SimpleXML, the easiest for fast parsing; DOMDocument, the most powerful for full manipulation; and XMLReader, the most efficient for large files. Knowing when to use each is the key.

Three XML Approaches in PHP #

flowchart TD
    A{XML needs} --> B{File size?}
    B -- "Small-Medium\n< a few MB" --> C{Need manipulation\nor just reading?}
    C -- "Read only\nfast parse" --> D[SimpleXML\nEasiest]
    C -- "Full manipulation\nadd/edit/delete nodes" --> E[DOMDocument\nMost powerful]
    B -- "Large\n> 10MB" --> F[XMLReader\nStreaming, memory-efficient]

    style D fill:#dcfce7
    style E fill:#dbeafe
    style F fill:#fef9c3

SimpleXML — Fast Parsing #

SimpleXML turns XML into PHP objects that can be accessed like properties and arrays:

<?php
$xml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<catalog>
    <product id="1" category="electronics">
        <name>Pro Laptop 14</name>
        <price currency="IDR">15000000</price>
        <stock>5</stock>
        <specs>
            <cpu>Intel Core i7</cpu>
            <ram>16GB</ram>
            <storage>512GB SSD</storage>
        </specs>
        <tags>
            <tag>laptop</tag>
            <tag>gaming</tag>
            <tag>work</tag>
        </tags>
    </product>
    <product id="2" category="accessories">
        <name>Ergo Pro Mouse</name>
        <price currency="IDR">350000</price>
        <stock>20</stock>
        <specs>
            <dpi>3200</dpi>
            <buttons>7</buttons>
        </specs>
        <tags>
            <tag>mouse</tag>
            <tag>wireless</tag>
        </tags>
    </product>
</catalog>
XML;

// Parse from a string
$catalog = simplexml_load_string($xml);

// Parse from a file
// $catalog = simplexml_load_file('catalog.xml');

// Access the first element
$firstProduct = $catalog->product[0];
echo $firstProduct->name;           // "Pro Laptop 14"
echo $firstProduct->price;          // "15000000"
echo $firstProduct->specs->cpu;     // "Intel Core i7"

// Access attributes
echo $firstProduct['id'];           // "1"
echo $firstProduct['category'];     // "electronics"
echo $firstProduct->price['currency']; // "IDR"

// Iterate repeating elements
foreach ($catalog->product as $product) {
    $id       = (string) $product['id'];
    $name     = (string) $product->name;
    $price    = (int) $product->price;
    $category = (string) $product['category'];

    echo "[$id] $name — Rp " . number_format($price) . " ($category)\n";
}

// Iterate tags (repeating elements inside an element)
foreach ($catalog->product[0]->tags->tag as $tag) {
    echo "Tag: $tag\n"; // laptop, gaming, work
}

// IMPORTANT: cast to PHP types
// SimpleXML returns SimpleXMLElement, not plain strings/ints
// Always cast when used outside a string context
$name  = (string) $product->name;   // string
$price = (int) $product->price;     // integer
$stock = (float) $product->stock;   // float
$inStock = ((int) $product->stock) > 0; // boolean

SimpleXML with Namespaces #

XML namespaces often appear in RSS, Atom, and SOAP:

<?php
$rss = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:media="http://search.yahoo.com/mrss/">
<channel>
    <title>PHP Blog</title>
    <item>
        <title>Learning PHP 8.3</title>
        <link>https://example.com/articles/php-83</link>
        <dc:creator>Budi Santoso</dc:creator>
        <dc:date>2024-03-15</dc:date>
        <media:thumbnail url="https://example.com/thumb.jpg" width="200" height="150"/>
    </item>
</channel>
</rss>
XML;

$feed = simplexml_load_string($rss);

// Access elements without namespaces — normal
echo $feed->channel->title; // "PHP Blog"
echo $feed->channel->item->title; // "Learning PHP 8.3"

// Access elements with namespaces — needs children() or registerXPathNamespace
$item = $feed->channel->item;

// Way 1: children() with the namespace URI
$dc    = $item->children('http://purl.org/dc/elements/1.1/');
$media = $item->children('http://search.yahoo.com/mrss/');

echo $dc->creator;   // "Budi Santoso"
echo $dc->date;      // "2024-03-15"
echo $media->thumbnail['url']; // "https://example.com/thumb.jpg"

// Way 2: XPath with namespaces (covered in the XPath section)

Converting SimpleXML to an Array #

<?php
function simpleXmlToArray(SimpleXMLElement $xml): array
{
    $result = [];

    foreach ($xml->attributes() as $name => $value) {
        $result['@' . $name] = (string) $value;
    }

    foreach ($xml->children() as $tag => $child) {
        $childArray = simpleXmlToArray($child);

        if (isset($result[$tag])) {
            // Already exists — make it an array
            if (!is_array($result[$tag]) || !isset($result[$tag][0])) {
                $result[$tag] = [$result[$tag]];
            }
            $result[$tag][] = $childArray;
        } else {
            $result[$tag] = $childArray;
        }
    }

    // Add the text value if it has no children
    $text = trim((string) $xml);
    if ($text !== '' && empty($result)) {
        return $text;
    }

    if ($text !== '') {
        $result['@value'] = $text;
    }

    return $result;
}

$catalog = simplexml_load_string($xml);
$array   = simpleXmlToArray($catalog);
echo json_encode($array, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

XPath — Querying Elements #

XPath is a query language for XML — like SQL but for XML documents:

<?php
$catalog = simplexml_load_string($xmlString);

// Basic XPath
$allProducts = $catalog->xpath('//product');
// All product elements anywhere in the document

$electronicsProducts = $catalog->xpath('//product[@category="electronics"]');
// Products with the attribute category="electronics"

$productNames = $catalog->xpath('//product/name');
// All name elements inside products

$pricesAboveFive = $catalog->xpath('//product[price > 5000000]');
// Products with a price > 5 million

// XPath with functions
$productsWithTag = $catalog->xpath('//product[tags/tag = "gaming"]');
// Products that have the tag "gaming"

$first  = $catalog->xpath('//product[1]'); // first element (1-based in XPath!)
$last   = $catalog->xpath('//product[last()]');

// XPath returns an array of SimpleXMLElement
foreach ($electronicsProducts as $product) {
    echo (string) $product->name . "\n";
}

// XPath with namespaces
$feed = simplexml_load_string($rssXml);
$feed->registerXPathNamespace('dc', 'http://purl.org/dc/elements/1.1/');
$feed->registerXPathNamespace('media', 'http://search.yahoo.com/mrss/');

$creators   = $feed->xpath('//dc:creator');
$thumbnails = $feed->xpath('//media:thumbnail');

foreach ($creators as $creator) {
    echo (string) $creator . "\n";
}

foreach ($thumbnails as $thumb) {
    echo $thumb['url'] . "\n";
}

DOMDocument — Full Manipulation #

DOMDocument gives you complete control over an XML document — read, modify, add, delete nodes, and generate new XML:

<?php
// Parse existing XML
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput       = true;

// Load from a string
$dom->loadXML($xmlString);

// Load from a file
// $dom->load('catalog.xml');

// DOM navigation
$catalog  = $dom->documentElement;       // root element
$product1 = $catalog->firstElementChild; // first child

// getElementsByTagName — all elements with a specific name
$allNames = $dom->getElementsByTagName('name');
foreach ($allNames as $name) {
    echo $name->textContent . "\n"; // Pro Laptop 14, Ergo Pro Mouse
}

// getElementById — needs an attribute declared as ID in a DTD/schema
// More common: use XPath

// DOMXPath for queries
$xpath   = new DOMXPath($dom);
$products = $xpath->query('//product[@category="electronics"]');

foreach ($products as $node) {
    $name  = $xpath->query('name', $node)->item(0)->textContent;
    $price = $xpath->query('price', $node)->item(0)->textContent;
    echo "$name: Rp " . number_format((int)$price) . "\n";
}

// evaluate() — XPath that returns scalar values
$productCount = $xpath->evaluate('count(//product)');
echo "Total products: $productCount\n"; // 2

$totalPrice = $xpath->evaluate('sum(//product/price)');
echo "Total price: Rp " . number_format($totalPrice) . "\n";

Modifying XML #

<?php
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->loadXML($xmlString);
$xpath = new DOMXPath($dom);

// Change an element's value
$stockNode = $xpath->query('//product[@id="1"]/stock')->item(0);
$stockNode->textContent = '3'; // change stock from 5 to 3

// Change an attribute
$productNode = $xpath->query('//product[@id="1"]')->item(0);
$productNode->setAttribute('updated', date('Y-m-d'));

// Add a new element
$newProduct = $dom->createElement('product');
$newProduct->setAttribute('id', '3');
$newProduct->setAttribute('category', 'furniture');

$newName  = $dom->createElement('name', 'Ergonomic Desk');
$newPrice = $dom->createElement('price', '2500000');
$newPrice->setAttribute('currency', 'IDR');
$newStock = $dom->createElement('stock', '8');

$newProduct->appendChild($newName);
$newProduct->appendChild($newPrice);
$newProduct->appendChild($newStock);
$dom->documentElement->appendChild($newProduct);

// Delete an element
$productToDelete = $xpath->query('//product[@id="2"]')->item(0);
$productToDelete->parentNode->removeChild($productToDelete);

// Save the result
$newXml = $dom->saveXML();
$dom->save('catalog_updated.xml');

echo $newXml;

Generating XML from Scratch #

<?php
// With DOMDocument
function createCatalogXml(array $productData): string
{
    $dom = new DOMDocument('1.0', 'UTF-8');
    $dom->formatOutput = true;

    $catalog = $dom->createElement('catalog');
    $catalog->setAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/');
    $dom->appendChild($catalog);

    foreach ($productData as $data) {
        $product = $dom->createElement('product');
        $product->setAttribute('id', $data['id']);
        $product->setAttribute('category', $data['category']);

        $name  = $dom->createElement('name');
        $name->appendChild($dom->createTextNode($data['name'])); // TextNode for automatic escaping
        $product->appendChild($name);

        $price = $dom->createElement('price', $data['price']);
        $price->setAttribute('currency', 'IDR');
        $product->appendChild($price);

        $stock = $dom->createElement('stock', $data['stock']);
        $product->appendChild($stock);

        $catalog->appendChild($product);
    }

    return $dom->saveXML();
}

// With XMLWriter — more concise for generating large XML
function createWithXmlWriter(array $productData): string
{
    $writer = new XMLWriter();
    $writer->openMemory();
    $writer->setIndent(true);
    $writer->setIndentString('    ');

    $writer->startDocument('1.0', 'UTF-8');
    $writer->startElement('catalog');

    foreach ($productData as $data) {
        $writer->startElement('product');
        $writer->writeAttribute('id', $data['id']);
        $writer->writeAttribute('category', $data['category']);

        $writer->writeElement('name', $data['name']);

        $writer->startElement('price');
        $writer->writeAttribute('currency', 'IDR');
        $writer->text($data['price']);
        $writer->endElement(); // price

        $writer->writeElement('stock', $data['stock']);

        $writer->endElement(); // product
    }

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

    return $writer->outputMemory();
}

$data = [
    ['id' => 1, 'name' => 'Laptop <Pro>', 'price' => 15000000, 'stock' => 5, 'category' => 'electronics'],
    ['id' => 2, 'name' => 'Mouse & Keyboard', 'price' => 500000, 'stock' => 10, 'category' => 'accessories'],
];

echo createWithXmlWriter($data);
// The < and & characters are automatically escaped to &lt; and &amp;

XMLReader — Streaming for Large Files #

When an XML file is too large to load into memory entirely, use XMLReader, which reads one node at a time:

<?php
// An XML file with millions of products
$reader = new XMLReader();
$reader->open('large_products.xml'); // or loadXML for strings

$count = 0;
$total = 0;

while ($reader->read()) {
    // Only process opening elements named 'product'
    if ($reader->nodeType !== XMLReader::ELEMENT || $reader->name !== 'product') {
        continue;
    }

    // Convert the current node to a SimpleXMLElement for easy access
    $node   = new SimpleXMLElement($reader->readOuterXml());
    $price  = (int) $node->price;
    $total += $price;
    $count++;

    // Jump to the next product element (skip the product's inner content)
    $reader->next('product');
}

$reader->close();

echo "Product count: $count\n";
echo "Total price: Rp " . number_format($total) . "\n";
// Memory usage: constant, regardless of file size

// XMLReader with namespaces
$reader = new XMLReader();
$reader->open('feed_with_namespace.xml');

while ($reader->read()) {
    if ($reader->nodeType === XMLReader::ELEMENT) {
        echo "Element: " . $reader->name . "\n";
        echo "Namespace: " . $reader->namespaceURI . "\n";
        echo "Local name: " . $reader->localName . "\n";

        if ($reader->hasAttributes) {
            while ($reader->moveToNextAttribute()) {
                echo "  Attribute: " . $reader->name . " = " . $reader->value . "\n";
            }
            $reader->moveToElement(); // back to the element
        }
    }
}

Parsing RSS/Atom Feeds #

<?php
function parseRssFeed(string $url): array
{
    $ctx = stream_context_create([
        'http' => ['timeout' => 10, 'user_agent' => 'PHP RSS Reader/1.0'],
    ]);

    $xmlString = file_get_contents($url, false, $ctx);
    if ($xmlString === false) {
        throw new \RuntimeException("Failed to fetch feed from: $url");
    }

    $feed = simplexml_load_string($xmlString);
    if ($feed === false) {
        throw new \RuntimeException("Feed is not valid XML");
    }

    $items = [];

    // Detect the format: RSS or Atom
    $isAtom = $feed->getName() === 'feed';

    if ($isAtom) {
        // Atom feed
        foreach ($feed->entry as $entry) {
            $items[] = [
                'title'   => (string) $entry->title,
                'link'    => (string) $entry->link['href'],
                'date'    => (string) $entry->updated,
                'summary' => (string) $entry->summary,
                'author'  => (string) $entry->author->name,
            ];
        }
    } else {
        // RSS 2.0
        $feed->registerXPathNamespace('dc', 'http://purl.org/dc/elements/1.1/');
        $feed->registerXPathNamespace('content', 'http://purl.org/rss/1.0/modules/content/');

        foreach ($feed->channel->item as $item) {
            $dc = $item->children('http://purl.org/dc/elements/1.1/');
            $items[] = [
                'title'   => (string) $item->title,
                'link'    => (string) $item->link,
                'date'    => (string) $item->pubDate,
                'summary' => strip_tags((string) $item->description),
                'author'  => isset($dc->creator) ? (string) $dc->creator : '',
            ];
        }
    }

    return $items;
}

// Usage
$news = parseRssFeed('https://example.com/feed.rss');
foreach ($news as $item) {
    echo "{$item['title']}\n";
    echo "  {$item['link']}\n";
    echo "  {$item['date']}\n\n";
}

Converting Between Arrays and XML #

<?php
// Array to XML
function arrayToXml(array $data, DOMDocument $dom, DOMElement $parent): void
{
    foreach ($data as $key => $value) {
        // Numeric keys become 'item'
        $tag = is_numeric($key) ? 'item' : $key;
        // Sanitize the tag name (remove invalid characters)
        $tag = preg_replace('/[^a-zA-Z0-9_\-.]/', '_', $tag);
        $tag = ltrim($tag, '0123456789.-');
        if (empty($tag)) $tag = 'item';

        if (is_array($value)) {
            $element = $dom->createElement($tag);
            $parent->appendChild($element);
            arrayToXml($value, $dom, $element);
        } else {
            $element = $dom->createElement($tag);
            $element->appendChild($dom->createTextNode((string) $value));
            $parent->appendChild($element);
        }
    }
}

function toXml(array $data, string $root = 'root'): string
{
    $dom = new DOMDocument('1.0', 'UTF-8');
    $dom->formatOutput = true;
    $rootElem = $dom->createElement($root);
    $dom->appendChild($rootElem);
    arrayToXml($data, $dom, $rootElem);
    return $dom->saveXML();
}

$data = [
    'user' => [
        'id'    => 1,
        'name'  => 'Budi & Friends',
        'email' => '[email protected]',
        'tags'  => ['php', 'mysql', 'redis'],
    ],
];

echo toXml($data, 'request');
/*
<?xml version="1.0" encoding="UTF-8"?>
<request>
  <user>
    <id>1</id>
    <name>Budi &amp; Friends</name>
    <email>[email protected]</email>
    <tags>
      <item>php</item>
      <item>mysql</item>
      <item>redis</item>
    </tags>
  </user>
</request>
*/

Summary #

  • SimpleXML for fast parsing — access elements like properties and attributes like arrays. Always cast to PHP types ((string), (int), (float)) because SimpleXMLElement isn’t a regular scalar type.
  • DOMDocument for full manipulation — add, modify, delete nodes; generate XML from scratch; more verbose but more powerful than SimpleXML.
  • XMLWriter for generating XML — more concise than DOMDocument for generating large documents, especially ones that don’t need manipulation after creation.
  • XMLReader for large files — streams one node at a time with constant memory regardless of file size. Combine readOuterXml() + SimpleXMLElement for easy per-node access.
  • XPath is the best way to query elements//product[@category="electronics"], count(//product), sum(//price). Use DOMXPath::evaluate() for expressions returning scalar values.
  • Namespaces need explicit handling — in SimpleXML use children($namespaceUri) or registerXPathNamespace(). In DOMXPath use registerNamespace().
  • createTextNode() not createElement('tag', $value) — for values that might contain special XML characters (<, >, &), use createTextNode() so they’re automatically escaped.
  • Detect RSS vs Atom formats — RSS uses <rss> as the root with <item> inside <channel>; Atom uses <feed> as the root with <entry>.

← Previous: Advanced Array Functions   Next: Image →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact