PHP XMLReader
What Is XMLReader? In simple terms: XMLReader reads an XML document like a cursor moving through the file, one node at a time. At each step, you look at where the cursor is ("a <product> starts
What Is XMLReader?
In simple terms: XMLReader reads an XML document like a cursor moving through the file, one node at a time. At each step, you look at where the cursor is ("a <product> starts here") and decide what to do: read a value, skip a whole section, or jump to the next record. Only the current position is in memory, so a 5 GB product feed is processed with the same small amount of memory as a 5 KB file.
Technically: XMLReader is a pull parser built on libxml: your code pulls the next node with read() (or jumps with next()), instead of receiving callbacks as with Expat. It is forward-only and read-only. Its most useful feature is expand(), which turns the current element into a DOM node, so each record can be processed comfortably with DOM or SimpleXML while the overall file is streamed.
XMLReader Compared
| SimpleXML / DOM | Expat (XML Parser) | XMLReader | |
|---|---|---|---|
| Memory | Whole document | Constant | Constant |
| Programming model | Navigate a tree | Callbacks push events to you | You pull nodes in a loop |
| Ease of use | Easiest | Hardest (manual state tracking) | Moderate; easy when combined with expand() |
| Typical use | Small/medium documents | Legacy streaming code | Large files, feeds, imports |
Opening a Document
$reader = new XMLReader();
$reader->open('/path/to/catalog.xml'); // from a file or URL
$reader->XML($catalogXml); // or from a string
PHP 8.4 adds static constructors with clearer names: XMLReader::fromUri($path), XMLReader::fromString($xml), and XMLReader::fromStream($resource).
Moving Through Nodes With read()
Each call to read() moves the cursor to the next node and returns false at the end of the document:
<?php
$reader = new XMLReader();
$reader->XML('<order id="501"><item>Pen</item></order>');
while ($reader->read()) {
$indent = str_repeat(' ', $reader->depth);
$line = match ($reader->nodeType) {
XMLReader::ELEMENT => "start <{$reader->name}>" . ($reader->hasAttributes ? ' id=' . $reader->getAttribute('id') : ''),
XMLReader::TEXT => "text \"{$reader->value}\"",
XMLReader::END_ELEMENT => "end </{$reader->name}>",
default => null,
};
if ($line !== null) {
echo $indent . $line . "\n";
}
}
$reader->close();
Output
start <order> id=501
start <item>
text "Pen"
end </item>
end </order>
Important Properties
| Property / Method | Meaning |
|---|---|
nodeType | Kind of node: XMLReader::ELEMENT, TEXT, END_ELEMENT, CDATA, SIGNIFICANT_WHITESPACE, … |
name / localName | Node name (with / without namespace prefix) |
value | Text value of text and attribute nodes |
depth | Nesting level (the root element has depth 0) |
hasAttributes, getAttribute($name) | Attribute access on element nodes |
isEmptyElement | true for self-closing elements like <br/> (no END_ELEMENT follows) |
readString() | The text content of the current element |
readOuterXml() / readInnerXml() | The current element's XML as a string |
Streaming Records: next() and expand()
Processing a large catalog record by record combines XMLReader's streaming with SimpleXML's convenience:
<?php
$reader = new XMLReader();
$reader->XML($catalogXml); // for a real file: $reader->open('catalog.xml')
$doc = new DOMDocument();
$processed = 0;
$lowStock = [];
// Move the cursor to the first <product> element
while ($reader->read() && $reader->name !== 'product') {
}
while ($reader->name === 'product') {
$product = simplexml_import_dom($doc->importNode($reader->expand(), true));
$processed++;
if ((int) $product->stock < 10) {
$lowStock[] = "{$product['sku']} {$product->name} ({$product->stock} left)";
}
$reader->next('product'); // jump to the next <product>, skipping the current subtree
}
$reader->close();
echo "Products processed: $processed\n";
echo "Low stock: " . implode(', ', $lowStock) . "\n";
Output
Products processed: 3
Low stock: LAMP-01 Desk Lamp (6 left)
Step-by-Step Explanation
- The first loop reads nodes until the cursor reaches the first
<product>element. expand()builds a DOM node for the current element only — one product with its children — not the whole document.importNode()adds it to a small helper document, andsimplexml_import_dom()converts it into aSimpleXMLElement, so the familiar$product->stockand$product['sku']syntax works.next('product')moves directly to the next sibling<product>, skipping the current product's inner nodes. When there are no more products, the cursor moves past them and the loop ends.- Memory use depends only on the size of one product, regardless of how many products the file contains.
In a real import, each product would be validated and inserted into the database (in batches, inside a transaction) instead of collected in an array.
Handling Errors
XMLReader reads lazily, so errors in a malformed document appear only when the cursor reaches them — possibly after many records were already processed:
<?php
libxml_use_internal_errors(true);
$reader = new XMLReader();
$reader->XML('<catalog><product sku="A"/><product sku="B"></catalog>');
$skus = [];
while (@$reader->read()) {
if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'product') {
$skus[] = $reader->getAttribute('sku');
}
}
$errors = libxml_get_errors();
echo "Read before failing: " . implode(', ', $skus) . "\n";
echo $errors === [] ? "Document OK\n" : "Error: " . trim($errors[0]->message) . "\n";
libxml_clear_errors();
Output
Read before failing: A, B
Error: Opening and ending tag mismatch: product line 1 and catalog
Both products were read before the error was detected at the closing tag. For imports, this means: process inside a database transaction and roll back if an error occurs, or validate the file first. XMLReader can also validate while reading against an XML Schema (setSchema()) or a RELAX NG schema (setRelaxNGSchema()).
When to Use XMLReader
| Use XMLReader | Use Something Else |
|---|---|
| Large feeds, exports, and imports (megabytes to gigabytes) | Small documents → SimpleXML |
| Reading only specific records or values from huge files | Complex queries across the whole document → DOM with XPath |
| Constant memory in long-running workers | Writing XML → XMLWriter |
Common Mistakes
Calling expand() on the Wrong Node
Effect: expand() on a text or end-element node returns nothing useful.
Fix: Expand only when nodeType is XMLReader::ELEMENT and the name matches.
Using read() Instead of next() After Processing a Record
Effect: The loop walks through every child node of the record you already processed.
Fix: Use next('elementName') to skip to the next sibling.
Forgetting Self-Closing Elements
Effect: Code waiting for an END_ELEMENT never sees one for <product/>.
Fix: Check isEmptyElement.
Assuming the Whole File Is Valid Before Processing
Effect: Partial imports when an error appears late in the file.
Fix: Use transactions or validate first.
Best Practices
- Use XMLReader for large XML files and streams.
- Jump between records with
next()and process each withexpand()plus SimpleXML or DOM. - Collect libxml errors and treat imports as all-or-nothing operations.
- Validate against schemas where the format defines one.
- Release resources with
close()in long-running scripts.
Official reference: https://www.php.net/manual/en/book.xmlreader.php