The root element have namespace declarations like xmlns:xlink="http://www.w3.org/1999/xlink"
... so, any node appended (ex. by appendChild
) will accept the namespace. I can append <graphic xlink:href=".."/>
because on the whole it is valid... But to append a fragment I need first to create the fragment with createDocumentFragment()
.
Example:
$tmp = $dom->createDocumentFragment();
$ok = $tmp->appendXML('<graphic xlink:href="file123.ext"/>');
when running, generates an error,
DOMDocumentFragment::appendXML(): namespace error :
Namespace prefix xlink for href on inline-graphic is not defined
How to say "use the DomDocument namespaces" to the DOMDocumentFragment::appendXML()
method?
NOTES AND CONTEXTS
(transfered as an answer, to not polute here)
It looks like it's working the way it's supposed to. Check out bug report #44773. chregu@php.net says it's not a bug and works properly. Though I would agree with the bug report and other comments, that since the fragment is made off of the DOMDocument, and it has the namespaces defined it should in fact know what they are and should work without problem.
Pass the namespace in with the element. It won't show up in the XML that is output, but will be read by the fragment so that it can create the attribute without any errors.
$dom = new DOMDocument('1.0', 'utf-8');
$root = $dom->createElement('MyRoot');
$root->setAttributeNS('http://www.w3.org/2000/xmlns/','xmlns:xlink','http://www.w3.org/1999/xlink');
$dom->appendChild($root);
$tmp = $dom->createDocumentFragment();
$ok = $tmp->appendXML('<graphic xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="file123.ext"/>');
$dom->documentElement->appendChild($tmp);
die($dom->saveXML());
Output
<?xml version="1.0" encoding="utf-8"?>
<MyRoot xmlns:xlink="http://www.w3.org/1999/xlink"><graphic xlink:href="file123.ext"/></MyRoot>
This is not a bug, it really is the expected behavior. Namespaces are not defined on or for a XML document, but on element nodes. They are valid for this node and any children until redefined.
So if you create the document fragment it has no parent node and now you append some XML fragment. Looking up it can not find any definition for the namespace and you get an error. Depending on where in the document you're going to add it, the namespace prefix could be used for completely different namespaces.
You have to define the namespace in the fragment, if the fragment is generated by DOM it should always have all needed namespace definitions.
If you generate it as text, you can make sure that the namespace definition is included in the element that needs it or you can add a wrapper element node with all needed namespace definitions.
$dom = new DOMDocument();
$dom->loadXml('<foo/>');
$fragment = $dom->createDocumentFragment();
$fragment->appendXML(
'<fragment xmlns:xlink="http://www.w3.org/1999/xlink">
<graphic xlink:href="file123.ext"/>
</fragment>'
);
foreach ($fragment->firstChild->childNodes as $child) {
$dom->documentElement->appendChild($child->cloneNode(TRUE));
}
echo $dom->saveXML();
Output:
<?xml version="1.0"?>
<foo>
<graphic xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="file123.ext"/>
</foo>
It is possible to generate the wrapper element from a list of namespaces. The following function will take an element node or a list of namespaces [prefix => namespace]
.
function wrapFragment($namespaces, $xml) {
if ($namespaces instanceOf DOMElement) {
$xpath = new DOMXpath($namespaces->ownerDocument);
$namespaces = $xpath->evaluate('namespace::*', $namespaces);
}
$result = '<fragment';
foreach ($namespaces as $key => $value) {
if ($value instanceOf DOMNamespaceNode) {
$prefix = $value->localName;
$xmlns = $value->nodeValue;
} else {
$prefix = $key == '#default' ? '' : $key;
$xmlns = $value;
}
$result .= ' '.htmlspecialchars(empty($prefix) ? 'xmlns' : 'xmlns:'.$prefix);
$result .= '="'.htmlspecialchars($xmlns).'"';
}
return $result.'>'.$xml.'</fragment>';
}
echo wrapFragment(
$dom->documentElement, '<graphic xlink:href="file123.ext"/>'
);
Output:
<fragment xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns:xlink="http://www.w3.org/1999/xlink"><graphic xlink:href="file123.ext"/></fragment>
NOTES about the question and the relation with PHP lack of resources for fragment namespaces.
A PHP bug?
(after @slapyo)
There are a PHP bug#44773: is not a "good behaviour", is a bug (!). If you agree please add a comment there, and vote up here (!).
Imagine you using a replace_innerXML($node,$innerXML)
function, or any other similar context... See "typical scenarios" section below.
How to workaround?
Without a big regular expression (over $innerXML
in the example) and slow algorithm, to set each tag without a namespace declaration... And, of course, after all, the appendXML()
turns the fragment a component of the tree, so no namespace is need because it is already at the root... all work is only to use the buggy fragments's appendXML()
.
Typical scenarios
(after @ThW answer/discussion) Typical "blind namespace" fragment uses.
When the fragment is a "extraterrenal" with a new namespace, ok, the fragment need to declare by itself the used namespaces... But the problem exposed in this question IS NOT this exotic one, is a so commom other one.
PS: as we will se, the "PHP bug"-solution also is a solution to this context.
Here, to illustrate, there are two typical uses of fragments where no a priori knowledge about namespaces (used by of fragment's elements) exist, only the fact that all have been declared in the DOMDocument (not need to redeclare).
1) a XSLT call-to-PHP-returning-fragment by XSLTProcessor::registerPHPFunctions()
;
2) a "generic DOM library" that offer a handling method for replace the XML inner contents of a node by a new XML content, that can be a fragment-content. See function replace_innerXML()
below.
function replace_innerXML(DOMNode $e, $innerXML='') {
if ($e && ($innerXML>'' || $e->nodeValue>'')) {
$e->nodeValue='';
if ($innerXML>'') {
$tmp = $e->ownerDocument->createDocumentFragment();
// HERE we need to INJECT namespace declarations into $innerXML
$tmp->appendXML($innerXML);
$e->appendChild( $tmp );
}
return true;
}
return false;
}
// This function is only illustrative, for other propuses
// see https://stackoverflow.com/q/26029868/287948
// Example of use:
$innerXML='nonoo <xx xx:aa="ww">uuu</xx> nono<a:yy zz:href="...">uu</a:yy>...';
replace_innerXML($someNode,$innerXML);
The algorithm "to INJECT namespace" (see comment in the function) would be simple if PHP features offered ... But, as we insist, PHP have a bug because offers nothing (!).
An elephantic solution
The only way (today 2014) to "INJECT namespace" is
- Step#1: detect all namespaces by a XPath query, obtaining a "namespace1", "namespace2", etc.
- Step#2: detect by insecure and inadequate regular expressions parsing (see also) all the namespaces, replacing it using somthing (perhaps more complex) like
$innerXML = preg_replace_callback(
"/([<\s])($namespacesJoinByPipe):([^\s>]+)/s",
function ($m) use($namespacesAssociative) {
$nsdecl = "xmlns:$m[2]=\"".$namespacesAssociative[$m[2]].'"';
return ($m[1]=='<')
? "<$m[1]$m[2]:$m[3] $nsdecl " // tag like "<a:yy"
: " $nsdecl $m[1]$m[2]:$m[3]"; // attribute like " xx:aa"
},
$innerXML
);
... So, is a big elephant to do a so simple thing: only to accept the pre-existent DOMDocument namespaces.
PHP have a bug because not avoid this "big elephant"... Solution to solve the "PHP bug"?
PHP's ideal solution
There are many alternatives for a PHP RCF to solve the problem: a flag createDocumentFragment($importRootNamespaces=false)
, a reference node createDocumentFragment($refNamespacesNode=NULL)
, or a DOMDocumentFragment::setAttributeNS() method
... All with a default behaviour equivalent to usual createDocumentFragment()
(no parameter).
PS: any of these solutions also help to handle other problems, like the first one comented, "... when the fragment is a 'extraterrenal' with a new namespace ...".
来源:https://stackoverflow.com/questions/26593430/how-to-use-global-namespace-definitions-in-a-fragment-creation