skip to content

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?

level: middleimportance: should knowfreq 30%

answer

  1. an unprefixed name means no namespace in XPath 1.0
  2. the default xmlns applies to elements
  3. registerNamespace with any prefix
  4. SimpleXML xpath(): registerXPathNamespace
  5. local-name() is a fragile workaround

basics

~10 s

In 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 s

A 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
<?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

for a junior

Recall that a feed with xmlns needs a registered prefix in DOMXPath or SimpleXML XPath queries.

for a middle

Explain why unprefixed names mean no namespace in XPath 1.0, how registerNamespace() and registerXPathNamespace() bind prefixes, and why attributes stay unprefixed.

for a senior

Diagnose a silent zero-row import after a supplier adds a namespace, fix the queries, and add a namespaced fixture to the test suite.

for a principal

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