(PHP 4)
xpath_eval — Evaluates the XPath Location Path in the given string
The optional contextnode can be specified for doing relative XPath queries.
See also xpath_new_context().
If the namespace is subject to change you can write even more portable code if you extend brandon dot whitehead at orst dot edu's solution like this:
$doc = domxml_open_mem($xml);
$xpath = $doc->xpath_new_context();
$namespace = $xpath->xpath_eval('namespace-uri(//*)')->value; // returns the namespace uri
xpath_register_ns($xpath, "pre", $namespace); // sets the prefix "pre" for the namespace
$obj = $xpath->xpath_eval('//pre:Offer'); // finds all Offer tags
$nodeset = $obj->nodeset;
print_r($nodeset);
This code will determine the namespace of the root element and set a prefix for XPath queries. Thus it doesn't matter if the namespace is changing in your XML (like some webservices do...)
Just an example of how to grab XML attributes with xpath - which took me a while to figure out. I'm filtering the returned object function node_content() which is a somewhat quick'n dirty solution, but I don't always need XML's child-parent relationships.
<?php
$xml='<MY_SERVICE>
<MERCHANDISE>
<SERVICE TYPE="books">
<NAME>Ulysses</NAME>
</SERVICE>
<SERVICE TYPE="books">
<NAME>The Poisonwood Bible</NAME>
</SERVICE>
<SERVICE TYPE="cars">
<NAME>Van</NAME>
</SERVICE>
<SERVICE TYPE="vehicle sans wheels">
<NAME>UFO</NAME>
</SERVICE>
</MERCHANDISE>
</MY_SERVICE>';
echo "<h4>XML</h4><xmp>";print_r(parse_XML($xml));echo"</xmp>";
function node_content($node,$attribute="content"){
foreach($node->nodeset as $content){
$return[] = $content->{$attribute};
}
return $return;
}
function parse_XML($xml){
//needs PHP's xPath extension installed
$dom =domxml_open_mem($xml);
$calcX = &$dom->xpath_new_context();
$xml_parsed["merchandise"]=node_content(
$calcX->xpath_eval("//MERCHANDISE/SERVICE/NAME/text()")
);
$xml_parsed["service"]=node_content(
$calcX->xpath_eval("//MERCHANDISE/SERVICE/attribute::TYPE",$calcX)
,"value");
return $xml_parsed;
}
?>
The code above returns:
XML
Array
(
[merchandise] => Array
(
[0] => Ulysses
[1] => The Poisonwood Bible
[2] => Van
[3] => UFO
)
[service] => Array
(
[0] => books
[1] => books
[2] => cars
[3] => vehicle sans wheels
)
)
In order to use the default namespace you must understand
how namespace prefixes work. Prefixes are simply convenient mappies to the namespace URI.
For example, if you set the namespace:
xmlns:xm="http://www.someurl.org"
and you have the following document fragment:
<rootnode><xm:childnode>Text</xm:childnode></rootnode>
this is essentially equivalent to:
<rootnode>
<http://www.someurl.org:childnode>
Text
</http://www.someurl.org:childnode>
</rootnode>
because the namespace URI is what matters, not the namespace prefix.
Unfortuantly, if you have a default namespace:
xmlns="http://www.anotherurl.org"
then all elements without a prefix belong to that namespace, and yet, it appears that PHP, and the underlying LIBXML2 don't let you register a default namespace with
"xpath_register_ns(context, prefix, uri)"
i.e. by leaving the prefix = "". Therefore, to get around the problem, simply give the default prefix a simple name, such as "pre".
For example, if you have a default namespace declaration such as the following document:
<?xml version="1.0" encoding="UTF-8"?>
<rootname xmlns="http://www.some.org" xml:lang="en-US">
<childnode>Some text</childnode>
</rootname>
And you want to evaluate the xpath expression:
"/rootname/childnode"
then you need to register the default namespace in PHP like this:
xpath_register_ns(context, "pre", "http://www.some.org");
and then use the following xpath expression:
"/pre:rootname/pre:childnode"
As you can see this is a lot prettier and more intuititive than using the local-name() function. In addition, it makes your code more portable, because you are guaranteed to always be working on nodes that belong to your explicitly stated namespace, uniquely identified by your URI.
Querying documents closed inside a namespace can be tricky
http://bugs.php.net/bug.php?id=11903
PHP Version: 4.3.1
I tried out how to get a part of a xml document with the xpath functions in domxml.
Try the following solution:
<?php
// get dom object
$xmldoc = domxml_open_mem($xml);
// init xpath
$xpath = xpath_new_context($xmldoc);
$xpresult = xpath_eval($xpath, "/root/info");
// dump all nodes directly in plain text
foreach ($xpresult->nodeset as $node)
{
$newxml .= $node->dump_node($node);
}
?>
If you wanna get a new dom object of the result just add
$newxmldoc = domxml_open_mem($newxml);
You can indeed use the result object of xpath_eval(). You just have to be careful to pass the result by reference! (note the ampersand's position).
$objXP = xpath_new_context($objDom)
$objTest = &xpath_eval($objXP,"//lalala");
$objTest->nodeset[0]->set_attribute("test","test data");
echo htlentities($objDom->dump_mem());
just be careful that is you pass around values from $objTest then they also need to be passed by reference.
If you want to apply an XPath-Expression to a particular node:
$ctx->xpath_eval("xpath",$node);
If you want to get the XPath for a particular node:
function getXPath($node) {
/* node id is held in a property named '1', this is
illegal in php so we use a workaround */
$one = '1';
$xpath = '';
while ($parent = $node->parent_node()) {
$siblings = $parent->child_nodes();
$index = 1;
foreach ($siblings as $sibling) {
if ($sibling->type != XML_ELEMENT_NODE || $sibling->tagname != $node->tagname) continue;
if ($sibling->$one == $node->$one) {
$xpath = '/' . $node->tagname . '[' . $index . ']' . $xpath;
break;
}
$index++;
}
$node = $parent;
}
return $xpath;
}
<?
$xml = xmldocfile('file.xml');
$xpath = $xml->xpath_new_context();
/**
* object access
*/
$ret = $xpath->xpath_eval('//tag');
/**
* function access
*/
$ret2 = xpath_eval($xpath, '//tag');
print_r($ret);
print_r($ret2);
?>
This function has come in handy for recursively viewing the results of xpath searches. It iterates through a node and converts it to a big associative array:
/**
* Recursive function to convert xml root node to big assoc array
*/
function xmlnode2array($node) {
if ($node->type==XML_ELEMENT_NODE) {
if ($attrArray = $node->attributes()) {
// parse attributes //
foreach($attrArray AS $attr) {
$out['ATTRIBUTE'][$attr->name] = $attr->value;
}
}
if ($childArray = $node->children()) {
// add child nodes //
foreach($childArray AS $child) {
if ($child->type==XML_ELEMENT_NODE) {
$out[$child->tagname][] = xmlnode2array($child);
} else {
if ($content = xmlnode2array($child))
$out['CONTENT'] = $content;
}
}
}
} else {
// this is a CONTENT NODE //
$out = trim($node->content);
if (!$out) return false;
}
return $out;
}
it seems that namespaces are not yet (PHP 4.06) implemented - xpath_eval($cnx,"/ns:tag") does work on w2k and does NOT on linux
I hope this little example helps someone out. If the XML data doesn't come thru in the post feel free to contact me via email.
<?
$p = xslt_create();
$o += 0;
$s = '';
$s .= "<query type='create'>";
$s .= "<resourceClass id='12345678901234567890' displayName='DAISY'>";
$s .= "<group family='global' id='kind'>";
$s .= "<node id='NODE_A' displayName='Red Ferrari' description='Red always goes faster'/>";
$s .= "</group>";
$s .= "</resourceClass>";
$s .= "<resourceClass id='12345678901234567890' displayName='BETTY'>";
$s .= "<group family='global' id='kind'>";
$s .= "<node id='NODE_B' displayName='Blue Porsche' description='But Porsches are a drivers car'/>";
$s .= "</group>";
$s .= "</resourceClass>";
$s .= "</query>";
$dom=xmldoc($s);
$ctx=xpath_new_context($dom);
$query_xo = xpath_eval($ctx,"count(/query/resourceClass)");
$num_rc = $query_xo->value;
echo("<BR>There are $num_rc classes in this list");
for($x=1; $x <= $num_rc; $x++){
$query_xo = xpath_eval($ctx,"/query/resourceClass[position()=$x]");
$query_ns = $query_xo->nodeset;
$resourceClass_dn = $query_ns[0];
// echo("<PRE>");
// print_r( $query_xo );
// echo("<PRE><HR>");
// print_r( $query_ns );
// echo("<PRE><HR>");
// print_r( $rc_dn );
echo("<BR>[id::".$resourceClass_dn->get_attribute('id')."][displayName::".$resourceClass_dn->get_attribute('displayName')."]");
}
?>
$ctx = xpath_new_context($doc);
$xpath_nodes = xpath_eval($ctx, "//some_element");
$xpath_nodes->nodeset[i]->set_content($string) allows you to set the node content. Try it and then do a $doc->dumpmem, you'll see the nodes in the original document are indeed updated properly.
I've used this feature lots. It does work.
xpath_eval() returns only a copy of your document. So you cant for example change the $foo->nodeset[id]->content's of the resulting matches. If you want to, you have to do it yourself by going recursive through your doc, which makes xpath_eval at least useless except for checking if you have to ;).
This is a very (very) minor point, but there is a comma missing in the function definition for xpath_eval. This being my first experience with xpath, I thought "object xpath context" was refering to a single parameter produced by a previous call to xpath_new_context(). Then I couldn't see where you would add the query (which is actually the context parameter)
So the proper definition should be
array xpath_eval (object xpath, context)
Additionally an example would be nice. I found one from a post to phpbuilder.com:
-------------------------------
http://www.phpbuilder.com/annotate/message.php3?id=1002772
-------------------------------
Message # 1002772:
Date: 01/02/01 06:40
By: Luis Argerich
Subject: new DOM features im 4.0.4
Just wanted to add that PHP 4.0.4 has improved DOM support including Xpath and
Xpointer support:
Try this:
$xml='SOME XML ....';
$doc=xmldoc($xml);
$ctx=xpath_new_context($doc);
$foo=xpath_eval($ctx,"//title");
print_r($foo);
It returns an object that contains a property called Nodeset with an array of DomNodes with the result of the Xpath expression. print_r($foo) to see the full structure.
4.0.4 has also added Xpointer support, so with Xpath and Xpointer support we can really do a lot of things from PHP to XML files.
Luis.