XML with SimpleXML
XML (eXtensible Markup Language) is a text format for structured data. It looks like HTML, but you invent your own tags. Most new web APIs use JSON, yet XML is still everywhere: RSS news feeds, the sitemap.xml that search engines read, SOAP web services used by banks and government systems, and office files (a .docx file is a zip of XML files). A PHP developer meets it sooner or later.
In this lesson you will read XML with SimpleXML, PHP's easiest XML tool, search it with XPath, create XML, handle broken files and namespaces, and use DOMDocument when you need more control. At the end you will see why JSON replaced XML for most APIs.
What XML looks like
Here is a small book catalogue. Save it as catalogue.xml; the examples below read it.
<?xml version="1.0" encoding="UTF-8"?>
<catalogue shop="Jaipur Book House">
<book id="b1" lang="en">
<title>Wings of Fire</title>
<author>A. P. J. Abdul Kalam</author>
<price currency="INR">350</price>
</book>
<book id="b2" lang="hi">
<title>Godaan</title>
<author>Premchand</author>
<price currency="INR">199</price>
</book>
<book id="b3" lang="en">
<title>Malgudi Days</title>
<author>R. K. Narayan</author>
<price currency="INR">275</price>
</book>
</catalogue>The first line is the XML declaration. There must be exactly one root element (<catalogue>) that contains everything else. Elements can have attributes (id="b1") and child elements. XML is strict: every tag must close, tags are case-sensitive, attribute values need quotes, and a & in text must be written &. A file that follows these rules is called well-formed.
Reading XML with SimpleXML
simplexml_load_file() reads a file and simplexml_load_string() reads a string. Both return a SimpleXMLElement object. Child elements become properties, repeated elements can be looped over or picked by index, and attributes are read like array keys:
<?php
declare(strict_types=1);
$catalogue = simplexml_load_file(__DIR__ . '/catalogue.xml');
echo $catalogue['shop'], "\n"; // an attribute
echo $catalogue->book[1]->title, "\n"; // second <book>, its <title>
echo count($catalogue->book), " books\n\n";
$total = 0;
foreach ($catalogue->book as $book) {
$price = (int) $book->price; // cast before doing maths
$total += $price;
echo "{$book['id']}: {$book->title} by {$book->author}, ₹$price\n";
}
echo "Total: ₹$total\n";
var_dump($catalogue->book[1]->title === 'Godaan');
var_dump((string) $catalogue->book[1]->title === 'Godaan');Jaipur Book House Godaan 3 books b1: Wings of Fire by A. P. J. Abdul Kalam, ₹350 b2: Godaan by Premchand, ₹199 b3: Malgudi Days by R. K. Narayan, ₹275 Total: ₹824 bool(false) bool(true)
The last two lines show the biggest SimpleXML trap. $book->title is not a string: it is another SimpleXMLElement object. echo converts it to text automatically, but === compares an object with a string and says false. Cast values with (string), (int) or (float) before comparing them, storing them in an array or doing maths.
Searching with XPath
XPath is a small query language for XML, a bit like CSS selectors. $xml->xpath() runs a query and returns an array of matching elements (an empty array if nothing matches). A few patterns cover most needs:
| XPath | Finds |
|---|---|
//book | Every <book> anywhere in the document |
/catalogue/book | <book> elements directly inside the root |
//book[@lang="en"] | Books whose lang attribute is en |
//book[price < 300] | Books with a <price> child below 300 |
//book/title | The <title> of every book |
<?php
declare(strict_types=1);
$catalogue = simplexml_load_file(__DIR__ . '/catalogue.xml');
// All English books
foreach ($catalogue->xpath('//book[@lang="en"]/title') as $title) {
echo $title, "\n";
}
// Books under ₹300
$cheap = $catalogue->xpath('//book[price < 300]');
echo count($cheap), " books under ₹300\n";
// One book by its id attribute
$found = $catalogue->xpath('//book[@id="b2"]');
echo $found[0]->title, ' by ', $found[0]->author, "\n";Wings of Fire Malgudi Days 2 books under ₹300 Godaan by Premchand
Comparisons with < and > in XPath work on numbers only. Dates written as text, such as 2026-10-25, cannot be compared that way; loop over the elements and compare them in PHP instead.
When the XML is broken
Files from other systems are not always well-formed. By default, a parsing error prints PHP warnings and the load function returns false. Turn on internal error handling so you can report problems your own way:
<?php
declare(strict_types=1);
$broken = '<order><item>Chai<item></order>';
libxml_use_internal_errors(true); // collect errors instead of printing warnings
$xml = simplexml_load_string($broken);
if ($xml === false) {
foreach (libxml_get_errors() as $error) {
echo 'Line ', $error->line, ': ', trim($error->message), "\n";
}
libxml_clear_errors();
}Line 1: Opening and ending tag mismatch: item line 1 and order Line 1: Premature end of data in tag item line 1
The messages come from libxml, the C library behind all of PHP's XML tools, so the exact wording can differ slightly between servers. Always check for false before you use the result.
Writing XML with SimpleXML
To create XML, start with a SimpleXMLElement holding just the root, then add children and attributes. asXML() returns the finished document as a string, or writes it to a file if you pass a file name:
<?php
declare(strict_types=1);
$order = new SimpleXMLElement('<order/>');
$order->addAttribute('id', 'ORD-1042');
$customer = $order->addChild('customer');
$customer->addChild('name', 'Farhan Qureshi');
$customer->addChild('city', 'Lucknow');
$item = $order->addChild('item');
$item->addAttribute('qty', '2');
$item->name = 'Rohan & Sons Pickle'; // assigning escapes & safely
$item->price = 240;
echo $order->asXML();<?xml version="1.0"?> <order id="ORD-1042"><customer><name>Farhan Qureshi</name><city>Lucknow</city></customer><item qty="2"><name>Rohan & Sons Pickle</name><price>240</price></item></order>
Watch the & in the shop name. addChild('name', 'Rohan & Sons') does not escape it: PHP shows the warning "unterminated entity reference" and leaves the element empty. Either assign the value through a property, as above, or escape it first with htmlspecialchars($value, ENT_XML1). Also notice that SimpleXML writes everything on one line. That is valid XML, but hard for humans to read.
DOMDocument: more control
The DOM extension gives you the full Document Object Model, the same idea as the DOM in JavaScript. It is wordier than SimpleXML but can do everything: pretty printing, namespaces, inserting nodes anywhere, and HTML. Here it builds a search-engine sitemap. Sitemaps use a namespace, a URL that says which vocabulary the tags belong to:
<?php
declare(strict_types=1);
$pages = [
['loc' => 'https://example.in/', 'lastmod' => '2026-10-28'],
['loc' => 'https://example.in/menu?veg=1&spicy=0', 'lastmod' => '2026-10-20'],
];
$doc = new DOMDocument('1.0', 'UTF-8');
$doc->formatOutput = true; // indent the result
$urlset = $doc->createElementNS('http://www.sitemaps.org/schemas/sitemap/0.9', 'urlset');
$doc->appendChild($urlset);
foreach ($pages as $page) {
$url = $doc->createElement('url');
$loc = $doc->createElement('loc');
$loc->appendChild($doc->createTextNode($page['loc'])); // text nodes are escaped for you
$url->appendChild($loc);
$url->appendChild($doc->createElement('lastmod', $page['lastmod']));
$urlset->appendChild($url);
}
echo $doc->saveXML();<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.in/</loc>
<lastmod>2026-10-28</lastmod>
</url>
<url>
<loc>https://example.in/menu?veg=1&spicy=0</loc>
<lastmod>2026-10-20</lastmod>
</url>
</urlset>createTextNode() escapes the & in the URL automatically. Namespaces affect XPath too: when you load this sitemap with SimpleXML, xpath('//loc') finds nothing, because loc lives in the sitemap namespace. Register a prefix first, then use it in the query: $xml->registerXPathNamespace('s', 'http://www.sitemaps.org/schemas/sitemap/0.9'); followed by $xml->xpath('//s:loc').
PHP 8.4 also added a modern, standards-compliant DOM API in the Dom namespace. Dom\XMLDocument::createFromFile() loads a file, and you can search with CSS selectors such as $doc->querySelectorAll('book[lang="en"] > title'), just like in the browser. Dom\HTMLDocument parses real-world HTML5 pages correctly, which the older DOMDocument never fully could.
Why JSON replaced XML for most APIs
During the 2000s and 2010s, most new web APIs moved from XML to JSON. The reasons are easy to see side by side:
| XML | JSON | |
|---|---|---|
| Size | Opening and closing tags repeat every name | Shorter: names appear once |
| In PHP | Objects you must cast, XPath, namespaces | json_decode() gives plain arrays |
| In JavaScript | Needs a parser and DOM code | Built in: JSON.parse(), fetch().json() |
| Data types | Everything is text | Numbers, booleans, null, arrays |
| Strong points | Attributes, mixed text, namespaces, schemas | Simple data exchange |
JSON maps directly onto arrays and objects in PHP and JavaScript, so it is simpler for the typical "send me some records" API. XML remains the better fit for documents (text with markup inside), for formats with strict schemas, and for older enterprise systems. For JSON in PHP, see JSON & APIs.
Common mistakes
- Comparing SimpleXML values without casting.
$book->title === 'Godaan'isfalse; cast with(string)first. - Not checking for
false. A broken file makessimplexml_load_file()returnfalse, and the next line fails. - Putting
&inaddChild()text. Assign through a property or escape withhtmlspecialchars($v, ENT_XML1). - Forgetting namespaces in XPath. Register a prefix with
registerXPathNamespace()for feeds and sitemaps. - Building XML by joining strings. One unescaped
<or&breaks the whole file; use SimpleXML or DOM.
Key points
- XML must be well-formed: one root, closed tags, quoted attributes,
&for&. - SimpleXML turns elements into properties and attributes into array keys; cast values before using them.
xpath()searches with paths like//book[@lang="en"]and returns an array of elements.- Use
libxml_use_internal_errors(true)andlibxml_get_errors()to handle broken input. SimpleXMLElement::addChild()andasXML()write XML;DOMDocumentadds pretty printing and full control.- JSON is simpler for data APIs; XML lives on in feeds, sitemaps, SOAP and documents.
Exercise
Load this XML with simplexml_load_string(): a <class name="10-B"> root with three <student> elements: Aarav (roll 1, marks 88), Meera (roll 2, marks 64) and Kabir (roll 3, marks 91), with roll and marks as attributes and the name as text. Add a fourth student, Isha (roll 4, marks 79). Then use XPath to print every student with 80 marks or more, and print the class average with one decimal place.
Show solution
<?php
declare(strict_types=1);
$xml = simplexml_load_string(<<<XML
<class name="10-B">
<student roll="1" marks="88">Aarav</student>
<student roll="2" marks="64">Meera</student>
<student roll="3" marks="91">Kabir</student>
</class>
XML);
$new = $xml->addChild('student', 'Isha');
$new->addAttribute('roll', '4');
$new->addAttribute('marks', '79');
foreach ($xml->xpath('//student[@marks >= 80]') as $s) {
echo "Topper: $s ({$s['marks']})\n";
}
$sum = 0;
foreach ($xml->student as $s) {
$sum += (int) $s['marks'];
}
printf("Average of %d students: %.1f\n", count($xml->student), $sum / count($xml->student));Topper: Aarav (88) Topper: Kabir (91) Average of 4 students: 80.5
The XPath @marks >= 80 compares attribute values as numbers. In the loop, (int) turns each attribute into a number before adding it up.
Quick Quiz
1. What does $catalogue->book[0]['id'] read?
In SimpleXML, square brackets with a name read an attribute.
2. Why is $book->title === 'Godaan' false even when the title is Godaan?
Cast with (string) before comparing, because SimpleXML returns objects.
3. Which XPath finds every book whose lang attribute is hi?
@ selects an attribute and the square brackets hold the condition.
4. What happens with addChild('name', 'Rohan & Sons')?
addChild() does not escape &; assign through a property or escape the text first.
Learn PHP with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
