In PHP, why does DOMXPath::query('//product') return an empty list for a feed that declares a default xmlns, and how do you fix it?
answer
- an unprefixed name means no namespace in XPath 1.0
- the default xmlns applies to elements
- registerNamespace with any prefix
- SimpleXML xpath(): registerXPathNamespace
- local-name() is a fragile workaround
basics
~10 sIn XPath 1.0 an unprefixed name matches only elements in no namespace, but a default xmlns puts every element into that namespace. Register a prefix with DOMXPath::registerNamespace('f', $uri) and query '//f:product'.
solid answer
~40 sA feed such as `<feed xmlns="https://supplier.example/ns/feed">` puts `<feed>`, `<product>` and every unprefixed descendant into that namespace. `DOMXPath` evaluates XPath 1.0, where a name without a prefix means "an element in **no** namespace", so `//product` matches nothing and `query()` returns an empty `DOMNodeList`, not an error. The fix is to bind any prefix you like to the same URI with `$xpath->registerNamespace('f', 'https://supplier.example/ns/feed')` and query `//f:product/f:name`. The prefix in your query does not need to match the document's; only the URI matters. SimpleXML's `xpath()` has the same rule, so call `registerXPathNamespace()` first; its property access, by contrast, still matches unprefixed elements in a default namespace. Rewriting queries with `local-name()` ignores namespaces entirely, which breaks when two vocabularies share element names.
code
php · 17 lines<?php
declare(strict_types=1);
$xml = '<feed xmlns="https://supplier.example/ns/feed">'
. '<product sku="A-100"><name>Desk lamp</name></product></feed>';
$dom = new DOMDocument();
$dom->loadXML($xml);
$xpath = new DOMXPath($dom);
var_dump($xpath->query('//product')->length); // int(0)
$xpath->registerNamespace('f', 'https://supplier.example/ns/feed');
foreach ($xpath->query('//f:product') as $product) {
$name = $xpath->evaluate('string(f:name)', $product);
echo $product->getAttribute('sku'), ': ', $name, "\n"; // A-100: Desk lamp
}go deeper
Recall that a feed with xmlns needs a registered prefix in DOMXPath or SimpleXML XPath queries.
Explain why unprefixed names mean no namespace in XPath 1.0, how registerNamespace() and registerXPathNamespace() bind prefixes, and why attributes stay unprefixed.
Diagnose a silent zero-row import after a supplier adds a namespace, fix the queries, and add a namespaced fixture to the test suite.
Decide how integrations pin and verify partner namespace URIs so schema changes fail loudly instead of producing empty imports.
## The symptom A supplier switches its product feed from ```xml <feed><product sku="A-100">...</product></feed> ``` to ```xml <feed xmlns="https://supplier.example/ns/feed"><product sku="A-100">...</product></feed> ``` Nothing else changes, yet the import finds zero products: `$xpath->query('//product')` returns an empty `DOMNodeList`. No warning, no exception. ## Why: two rules meeting 1. **A default namespace declaration applies to elements.** `xmlns="..."` on `<feed>` places `<feed>` and every unprefixed descendant element in that namespace. (Attributes without a prefix are not affected; `sku` stays in no namespace.) 2. **XPath 1.0 has no default namespace for names in expressions.** In `//product`, the unprefixed name means "a `product` element in no namespace". `DOMXPath` implements XPath 1.0 through libxml, so that rule applies. The elements are now `{https://supplier.example/ns/feed}product`, and the query asks for `{}product`. They are different names. ## The fix in DOMXPath `DOMXPath::registerNamespace(string $prefix, string $namespace): bool` binds a prefix for use in expressions: ```php $xpath = new DOMXPath($dom); $xpath->registerNamespace('f', 'https://supplier.example/ns/feed'); $products = $xpath->query('//f:product'); ``` Points worth knowing: - the prefix is **yours**; it does not have to match anything in the document, which has no prefix at all here; - every step needs it: `//f:product/f:name`, not `//f:product/name`; - attributes stay unprefixed: `//f:product[@sku="A-100"]`; - `query()` returns a `DOMNodeList` (empty when nothing matches) and `false` for a malformed expression, while `evaluate()` also returns scalar results such as `count(//f:product)`. The 8.4 `Dom\XPath` class has the same `registerNamespace()` method. ## The same problem in SimpleXML SimpleXML behaves differently for its two access styles: | Operation | Without a namespace | With a default namespace | With a prefixed namespace (`s:product`) | |---|---|---|---| | XPath | `$feed->xpath('//product')` | `registerXPathNamespace('f', $uri)`, then `xpath('//f:product')` | the same registration and query | | property navigation | `$feed->product` | `$feed->product` still works | `$feed->children($uri)->product` | SimpleXML's property access matches elements with no namespace **prefix**, which includes elements in a default namespace, so the code that walks `$feed->product` keeps working while the XPath queries silently return nothing. That split is exactly why the bug is confusing when it appears. ## Why local-name() is a trap A common workaround is `//*[local-name()='product']`. It matches regardless of namespace, which feels robust but: - it also matches a `product` element from any other vocabulary mixed into the document; - it is harder to read and slower than a plain name test; - it hides a contract change (a new namespace URI) that you probably want to notice. ## Making it robust - Read the namespace URI from your integration contract and register it once when you create the `DOMXPath`. - If suppliers vary, read the root's namespace with `$dom->documentElement->namespaceURI` and decide explicitly what you accept. - Add a test fixture with the namespace declared, since a namespace-free fixture passes while production fails. ## Quick reference | Goal | DOM call | |---|---| | bind a prefix | `$xpath->registerNamespace('f', $uri)` | | select elements | `$xpath->query('//f:product')` | | read a scalar | `$xpath->evaluate('string(f:name)', $contextNode)` | | count matches | `$xpath->evaluate('count(//f:product)')` | | check the root's namespace | `$dom->documentElement->namespaceURI` | Registering the prefix once, right after creating the `DOMXPath`, keeps every query in the importer consistent.
- Does the prefix you pass to registerNamespace() have to match the one used in the document?No. XPath matches on the namespace URI, and the prefix is only a local alias for your expression. A document using a default namespace has no prefix at all, and a document using `s:product` matches `//f:product` as long as `f` is bound to the same URI.
- Why does //f:product[@f:sku] fail to match even after registering the prefix?Unprefixed attributes are not in the default namespace; they have no namespace. So the attribute test must be `@sku`, not `@f:sku`. Only element names take the prefix.
saying these in an interview costs you the question
- Believing an unprefixed XPath name matches the document's default namespace
- Thinking the registered prefix must equal the document's prefix
- Prefixing attributes that have no prefix in the document
- Treating local-name() as a safe general fix
- Expecting DOMXPath::query() to warn when nothing matches