DOM is useful when you need to change an XML document rather than only read its values. PHP's DOMDocument represents the document as a tree of nodes. You can create elements, append text, remove an attribute, or locate nodes using DOMXPath. This reference walks through a small inventory update and shows how to preserve the difference between text and markup. You will also learn about node ownership, live collections, namespaces, and parsing errors. The examples use the established DOMDocument API, which remains widely available; newer DOM APIs have separate version requirements and should not be mixed casually into the same tutorial.
Think in nodes and relationships
A DOM document owns its nodes. An element can contain other elements and text nodes; an attribute belongs to an element. appendChild() places a node in a parent, and removeChild() removes a child from that parent. nodeValue and textContent describe content, while saveXML() serializes structure into XML syntax. A string containing '<b>Pen</b>' is not automatically a child element: treat it as text unless you deliberately parse and import markup under a trusted policy. createTextNode() makes that intent explicit and escapes characters when serialized. XML has a single document element, although processing instructions and comments may surround it. Check whether a queried node is present before accessing properties, because a valid document may not contain the business field you expected.
Create XML without joining untrusted strings
Construct an element with createElement(), attach attributes with setAttribute(), and add plain values through createTextNode(). This avoids manually replacing ampersands or angle brackets and avoids accidentally turning data into XML structure. For example, a product named 'Pen & Pad' should become text containing an ampersand, which the serializer represents as &. Never concatenate an untrusted name directly into '<name>' plus the value plus '</name>'. A name containing markup could change the structure or produce invalid XML. Select an encoding and ensure your input strings actually use it; declaring UTF-8 does not repair arbitrary bytes. Decide whether to include an XML declaration when sending a document, and set an appropriate application/xml Content-Type before writing it to an HTTP response.
Parse and query with explicit failure handling
DOMDocument::loadXML() returns a boolean. A try/catch alone does not guarantee every malformed document becomes an exception, so check its result. Use libxml internal errors to keep parser diagnostics from mixing with the intended response, then clear and restore the error state. Reject an empty input before parsing because it has its own warning behavior. DOMXPath evaluates expressions against the tree and returns node selections for path queries. An expression such as /inventory/product[@code='0012'] can find the desired product, but request-supplied XPath fragments need their own safe construction policy. Prefer traversal or a correctly quoted literal over concatenating arbitrary input into an expression. Parsing success does not prove the inventory complies with your required schema.
Handle namespaces and document ownership
DOMXPath needs a registered prefix for namespace-aware queries, including documents using a default namespace. Register your own prefix bound to the namespace URI; the source document's visible prefix does not have to match. createElementNS() and setAttributeNS() create namespace-aware nodes, unlike simply adding a colon to an ordinary element name. A node created by one document cannot always be attached directly to another document. importNode() copies it into the receiving document, with a deep option when descendants should come along. adoptNode() has different ownership behavior where supported, so choose intentionally. Test a feed that changes its prefix but retains the same URI. That catches code that accidentally depends on presentation rather than expanded element names.
Modify collections without skipping nodes
Some DOM node collections are live: removing one node changes the collection while you iterate. A forward index loop can then skip the node that moved into the removed position. For a known live collection, iterate backward or copy the target nodes into a stable list before removal. Query results and APIs differ in their collection behavior, so do not assume every list is identical. When updating a value, decide whether you want to replace all content or only one text node. Setting textContent removes descendants, which is appropriate for a simple leaf field but destructive for rich structured content. Validate the resulting document before saving if your consumer expects a particular structure. FormatOutput improves readability but is not a guarantee of preserving original whitespace.
Bound XML features and memory
DOM holds a document tree, so memory grows with input size and node count. Limit accepted bytes and avoid disabling parser safety limits merely to accommodate an unbounded feed. Untrusted documents should not enable unnecessary DTD loading or entity expansion. If your contract forbids DOCTYPE, reject it; do not turn on LIBXML_NOENT to satisfy a document you never intended to support. LIBXML_NONET is useful but does not replace a complete input policy. DOM is suitable for bounded configuration documents and editing jobs; a streaming parser is a better fit for very large independent records. Serialization can change quote styles, entity spelling, whitespace, or declaration details. If exact original bytes are required for signatures, preserve them and follow the signature format's canonicalization rules.
Practice task and worked solution
Load a bounded inventory XML string and remove every product whose discontinued attribute is 'yes'. Collect matching nodes into a stable array before removing them, then serialize the result. The solution checks loadXML(), applies the application's no-DTD policy, and handles a document with no matching nodes. Test two adjacent discontinued products to catch a skipped-node bug. Add a new product using createTextNode() for its name and importNode() if its subtree came from another document. Validate required stock values before saving. Report an invalid document separately from a valid document with no products, because those outcomes have different meanings.
Continue learning
Review file handling for saving snapshots and exceptions for cleanup after parsing failures.
The DOMDocument loadXML reference documents parsing results and options.
XML Processing with DOM Examples
Each example shows the code, the output it produces and what each part does.
Create a product and update its stock
This CLI example uses DOM only and requires the DOM extension. Text nodes preserve the ampersand as data.
Expected XML contains <name>Pen & Pad</name> and <stock>7</stock> inside the product with code 0012. The exact insignificant whitespace is not important to the tree's meaning. The text ampersand is escaped by serialization rather than by manual replacement.
<?php
$document = new DOMDocument('1.0', 'UTF-8');
$root = $document->createElement('inventory');
$document->appendChild($root);
$product = $document->createElement('product');
$product->setAttribute('code', '0012');
$root->appendChild($product);
$name = $document->createElement('name');
$name->appendChild($document->createTextNode('Pen & Pad'));
$product->appendChild($name);
$stock = $document->createElement('stock');
$stock->appendChild($document->createTextNode('4'));
$product->appendChild($stock);
$query = new DOMXPath($document);
$node = $query->query('/inventory/product[@code="0012"]/stock')->item(0);
if ($node === null) throw new RuntimeException('Missing stock');
$node->textContent = '7';
echo $document->saveXML($root);<inventory><product code="0012"><name>Pen & Pad</name><stock>7</stock></product></inventory>XML Processing with DOM FAQ
Should I manually escape text first?
No. Create text nodes with createTextNode() or set textContent, and DOM escapes characters such as & and < for you when the document is saved. Escaping the text yourself first leads to double escaping, so & appears as &amp; and the content is wrong.
Why does a node belong to another document?
DOM tracks which document owns every node, and a node from one document can't simply be appended to another, which raises a "Wrong Document" error. Import it into the receiving document with importNode($node, true) first, then append the imported copy.
Why do namespace queries miss nodes?
Elements in a namespace aren't matched by plain names in XPath. Register a prefix for their namespace URI with DOMXPath::registerNamespace() and use it in the query, such as //p:item. The prefix you choose doesn't have to match the one in the document; the URI is what counts.
Does pretty printing preserve original bytes?
No. Formatting output adds or changes whitespace between elements, and saving may also change attribute quoting or the XML declaration. Keep the source bytes when exact representation matters, for example when a signature covers the document, and only reformat copies meant for people to read.
XML Processing with DOM Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Removing adjacent nodes from a live list skips one. Which fix works?
XML Processing with DOM Quiz
2 questions to check what you learned.
Which API represents user text safely in XML?
- Concatenating it into raw XML
- eval()
- settype()
- createTextNode()
How can nodes from another document be copied in?
- importNode() on the receiving document
- Renaming the file
- Changing the PHP variable name
- Casting the node to an integer