Search code examples
phpxmlnamespacesprefix

(PHP) Append new XML child element(s) with namespace prefix


I have a XML that has a t:subscriptions element that holds multiple t:subscription child elements.

My main goal is to replace current t:subsciptions as a whole or all t:subsciption elements with new ones. At the moment I have created new t:subscription elements as strings (didn't get a grasp of dom/simplexml classes so I had to use "fallback" method).

Now I'm trying to convert string to XML by simplexml_load_string and then dom_import_simplexml in order to append/replace current subscriptions in XML. I've tried following example found from this site:

$newsubs = ''; //new XML as string

$sdom = new DOMDocument();
$sdom->loadXML($xml->asXML()); // current XML
//dpm($sdom->saveXML());
$xpath = new DOMXPath($sdom);

// ... x functions generate $newsubs string ...

$remove = $sdom->getElementsByTagName('subscriptions')->item(0); // get t:subscriptions for removal
$remove->parentNode->removeChild($remove); // remove t:subscriptions
$fragment = $sdom->createDocumentFragment();
$fragment->appendXml($newsubs);
$xpaths
->evaluate('//t:member')        // this node holds t:subscriptions among others
->item(0) // there's always only one member node
->appendChild($fragment);
debug($sdom->saveXML());

I've come so far that only error received is Namespace prefix X on Y is not defined and this is for all elements. Script removes namespace prefixes although they are defined in xml string eg <t:myelement>asdasd</t:myelement>.

The XML document that this XML is going to be added is already prefixed. I don't get it why it can't accept prefixed XML and strips prefix away and yet new XML get appended to the document? Doesn't make sense.


Solution

  • The error message can mean that the "xmlns:*" attribute for the namespace is missing in the XML or that the namespace prefix was not registered on the XPath object.

    XML Document

    Namespaces need to be defined on both sides. The first is the XML itself.

    <foo xmlns="urn:foo"/>
    

    or

    <f:foo xmlns:f="urn:foo"/>
    

    Are both resolved by the parser to the same namespace and local name. You can read the element name as {urn:foo}:foo.

    Here are several *NS methods on the DOM that allow you to work with a namespace. The namespace is always the URN. The prefix is not relevant for matching.

    $node->getElementsByTagNameNS('urn:foo', 'foo')
    

    The prefix is only provided if it is needed as a value. For example to create an element node that uses it.

    $document->createElementNS('urn:foo', 'foo'); // no prefix 
    $document->createElementNS('urn:foo', 'f:foo'); // with prefix "f"
    

    XPath

    XPath uses the prefixes in the expressions, too. But it should not read them from the document. Use DOMXpath::registerNamespace() to register an own prefixes for a specific namespace.

    Example

    Your question is missing the full XML so I will add a small Atom example. You can see that the namespace is the same in the XML and in the PHP, but the prefix is different:

    $xml = <<<'XML'
    <?xml version="1.0" encoding="UTF-8"?>
    <atom:feed xmlns:atom="http://www.w3.org/2005/Atom">
      <atom:title>Example Feed</atom:title>
      <atom:entry>
        <atom:title>Atom-Powered Robots Run Amok</atom:title>
        <atom:id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</atom:id>
      </atom:entry>
    </atom:feed>
    XML;
    
    $dom = new DOMDocument();
    $dom->loadXml($xml);
    $xpath = new DOMXPath($dom);
    $xpath->registerNamespace('a', 'http://www.w3.org/2005/Atom');
    
    var_dump($xpath->evaluate('string(//a:entry/a:title)'));
    

    Output:

    string(28) "Atom-Powered Robots Run Amok"