XML in PHP: A Quick Tour, and Where to Go Deeper
Get a working sense of PHP's XML options (SimpleXML for quick reads, DOM for full manipulation, namespaces as an unavoidable detail) and know, at minimum, that untrusted XML input carries an XXE risk that modern PHP defaults protect against -- with the full depth of every API and the XXE security model covered in the dedicated XML Ecosystem lesson.
XML remains common in integration work -- SOAP APIs, feeds, sitemap files, legacy system exports -- and PHP's two main entry points are SimpleXML (quick, convenient reads of small documents) and DOMDocument (the full tree API, needed for namespaces, XPath, validation, and read/write manipulation). This lesson is a brief, practical overview to place XML alongside templates, email, and CSV as one of the output/data formats a PHP application routinely produces and consumes; the complete picture -- DOM vs. SimpleXML tradeoffs in depth, XMLReader/XMLWriter streaming for large documents, XPath, namespace handling, schema validation, and a full treatment of XXE -- is covered in the XML Ecosystem lesson, and this lesson does not repeat that depth.
SimpleXML for a quick read; DOM when you need more
simplexml_load_string() turns a small XML document into an object tree where child elements read like object properties ($catalog->book[0]->title), which is the fastest path to pulling a handful of values out of a document whose shape you already know -- a small config file, a simple feed. The moment a task needs XPath queries, namespace-aware traversal, schema validation, or building/modifying a document programmatically, DOMDocument is the right tool instead; both APIs load the full document into memory as a tree, which is fine at small-to-moderate sizes and the reason the XML Ecosystem lesson introduces XMLReader/XMLWriter for anything too large to hold in memory that way.
Namespaces are worth knowing about even at this overview level: a namespaced document (xmlns:c="...") requires namespace-aware access -- SimpleXML's children($namespaceUri) or DOM's DOMXPath::registerNamespace() -- because a bare, unqualified property or tag-name lookup silently matches nothing against elements that are actually in a namespace. This is a frequent source of 'my XML parsing returns nothing' bugs that has nothing to do with the parsing code being wrong and everything to do with the namespace not being accounted for.
XXE, briefly -- and where the full treatment lives
XML's DOCTYPE mechanism can declare an external entity that references a URI to be fetched and substituted into the parsed document -- if a parser resolves such an entity against untrusted, attacker-supplied XML, that can leak local file contents or perform server-side request forgery against internal services. Modern PHP (via libxml2 2.9+, true on every currently supported version) disables external entity loading by default, so ordinary use of DOMDocument/SimpleXML on untrusted input is already XXE-safe out of the box; the real risk today is code that explicitly re-enables it by combining flags like LIBXML_NOENT with LIBXML_DTDLOAD when loading a document -- don't do that on input you don't fully trust.
That single paragraph is intentionally the extent of the security discussion here -- the XML Ecosystem lesson covers XXE in full, including exactly which flag combinations are dangerous, how to structure a secure parsing function for untrusted XML sources (file uploads, webhook payloads, third-party API responses), and how validation errors are retrieved via libxml_get_errors(). Read that lesson before writing any code that parses XML from a source you don't fully control.
• Modern PHP (libxml2 2.9+): external entity loading is OFF by default -- ordinary DOMDocument/SimpleXML use on untrusted input is already XXE-safe. • The risk today is explicit re-enablement: combining LIBXML_NOENT with LIBXML_DTDLOAD when loading untrusted XML re-opens the hole. • Full XXE mechanics, secure-parsing patterns, XPath, streaming (XMLReader/XMLWriter), namespaces, and schema validation: see the XML Ecosystem lesson.
Code Examples
simplexml_load_string() parses the document into a tree where $catalog->book iterates every <book> child, $book['id'] reads an attribute, and $book->title reads a child element -- all with an explicit cast to (string)/(float) since SimpleXML values are SimpleXMLElement objects internally rather than plain scalars, and comparing or formatting them directly without casting can produce surprising results. Expected result: #1: PHP Basics ($19.99) #2: Advanced PHP ($34.5) foreach ($catalog->book as $book) {
<?php
declare(strict_types=1);
$xml = <<<XML
<catalog>
<book id="1"><title>PHP Basics</title><price>19.99</price></book>
<book id="2"><title>Advanced PHP</title><price>34.50</price></book>
</catalog>
XML;
$catalog = simplexml_load_string($xml);
foreach ($catalog->book as $book) {
$id = (string) $book['id'];
$title = (string) $book->title;
$price = (float) $book->price;
echo "#$id: $title (\$$price)" . PHP_EOL;
}Important Rules
Reach for SimpleXML for a quick read of a small, well-known document; reach for DOMDocument once you need XPath, namespaces, validation, or programmatic tree manipulation.
Never assume a bare/unqualified element lookup will match elements that are actually in an XML namespace -- use namespace-aware access explicitly.
Do not parse untrusted XML with flags that combine LIBXML_NOENT and LIBXML_DTDLOAD -- that re-enables external entity resolution that modern PHP disables by default.
For anything beyond this overview -- streaming large documents, XPath, schema validation, and the full XXE security model -- see the XML Ecosystem lesson rather than reinventing it here.
Where this shows up
XML shows up in PHP applications wherever they integrate with older enterprise systems, SOAP-based APIs, RSS/Atom feeds, or sitemap generation -- a smaller footprint than JSON in greenfield APIs today, but still unavoidable in B2B and legacy-system integration work. Treat this overview as the 'know it exists and where the security line is' pass, and the XML Ecosystem lesson as the reference to actually build XML-handling code from.
$dom->loadXML($untrustedXmlFromUpload, LIBXML_NOENT | LIBXML_DTDLOAD);
$dom->loadXML($untrustedXmlFromUpload); // modern PHP defaults already disable external entity loading
Explicitly passing LIBXML_NOENT together with LIBXML_DTDLOAD re-enables external entity resolution for that parse, reopening the XXE hole that PHP's modern default (external entity loading disabled) already closes -- there's rarely a legitimate reason to combine both flags on XML from a source you don't fully control.
[multiple-choice] A developer parses XML uploaded by an untrusted user with $dom->loadXML($xml, LIBXML_NOENT | LIBXML_DTDLOAD). What is the concern? A. loadXML() will always throw a fatal error with these flags B. Combining LIBXML_NOENT with LIBXML_DTDLOAD explicitly re-enables external entity resolution, reopening the XXE vulnerability that modern PHP's default (external entity loading disabled) otherwise protects against on untrusted input C. These flags only affect XML output formatting, not parsing behavior D. SimpleXML must be used instead of DOM for any untrusted input
B. Combining LIBXML_NOENT with LIBXML_DTDLOAD explicitly re-enables external entity resolution, reopening the XXE vulnerability that modern PHP's default (external entity loading disabled) otherwise protects against on untrusted input — PHP's modern default already disables external entity loading, making ordinary parsing of untrusted XML XXE-safe out of the box. Explicitly combining LIBXML_NOENT (substitute entities) with LIBXML_DTDLOAD (load the external DTD) undoes that protection for that specific parse call, which is the actual source of most real XXE vulnerabilities in current PHP code.