I want to replace a foreach loop using a xpath expression, but I need that a DOMXPath object to return more than one list.
I have the following XML (simplified) and I using DOMDocument and DOMXPath to iterate over it:
<a:RoomsType>
<a:Rooms>
<a:Room>
<a:RPH>0</a:RPH>
</a:Room>
<a:Room>
<a:RPH>1</a:RPH>
</a:Room>
<a:Room>
<a:RPH>2</a:RPH>
</a:Room>
<a:Room>
<a:RPH>0</a:RPH>
</a:Room>
<a:Rooms>
<a:RoomsType>
I want to split the rooms by the RPH number, creating a list of rooms for each RPH number. Currently, I'm using the following code:
//$xpath is a DOMXPath object
$roomsToIterate = $this->xpath->query("//a:RoomsType/a:Rooms/a:Room");
$roomList = array();
foreach ($roomsToIterate as $room) {
$rphCandidate = $room->getElementsByTagName("RPH")->item(0)->nodeValue;
if (!isset($roomList[$rphCandidate])) {
$roomList[$rphCandidate] = array();
}
$roomList[$rphCandidate][] = $room;
}
This is working for now, but I want to replace the foreach loop with a Xpath expression. I can use the expression $rooms = $this->xpath->query("//a:RoomsType/a:Rooms/a:Room[a:RPH='{$rph}']"); with $rph being a number, but how can I do it if I don't know the RPH (it could be anything between 0 and 99). Is it possible?
In short, Are there any way to replace my foreach loop using XPath?
I was thinking about the use of registerPhpFunctions and a custom function, but I concerned about the performance of this approach compared with foreach loop
Xpath 1.0 expression will return a list of nodes, they can to some extend flatten an existing structure if you use an axis like descendant or ancestor, but it will be a list of nodes. It can not group or aggregate them.
You could fetch a lists of nodes with a specific RPH value. But you would need to this for each value, the result would be another loop. This would mean to fetch all RPH values, make them unique, iterate them and execute and Xpath expression for each value.
Your current solution is fine.
Related
I am using Simple HTML Dom parser to get an element from an HTML string using it's class name, like:
foreach ($html->find('div[class=news-div]')) {
$news = $news-div;
}
But I also need to get two elements (one is span and the other is a) that occur just before the $news, but they don't have an id that can be predicted because it is calculated dynamically, and they don't have a unique class name.
How can I extract the two adjacent elements occurring before $news-div?
SimpleHTML has prev_sibling and next_sibling methods
$elems = $html->find('div[class=news-div]');
foreach ( $elems as $news ) {
$prev_span = $news->prev_sibling();
$prev_a = $prev_span->prev_sibling();
}
For a website i'm making i need to get data from an external XML file.
I load the data like this:
$doc = new DOMDocument();
$url = 'http://myurl/results/xml/12345';
if (!$doc->load($url))
{
echo json_encode(array('error'=> 'error'));
exit;
}
$xpath = new DOMXPath($doc);
$program_date = $xpath->query('//game/date');
Then i use a foreach loop to get all the data
if($program_date){
foreach($program_date as $node){
$programArray['program_date'][] = $node->nodeValue;
}
}
The problem i'm having is that sometimes a certain game doesn't have a date.
So when a game doesn't have a date, i just want it to put "-", instead of the date from the XML file. My problem is that i don't know how to check if a date is present in the data.
I used a lot of ways like isset, !isset, else, !empty, empty
$teamArray['program_kind'][] = "-";
but noting works...
Can someone help me with this problem?
Thanks in advance
You need to iterate the game elements, use them as a context and fetch the data with additional XPath expressions.
But one thing first. Use DOMXPath::evaluate(). DOMXPath::query() only supports location paths. It can only return a node list. But XPath expressions can return scalar values, too.
$xpath = new DOMXPath($doc);
$games = $xpath->evaluate('//game');
The result of //game will always be a DOMNodeList object. It can be an empty list, but you can directly iterate it. A condition like if ($games) will always be true.
foreach ($games as $game) {
Now that you have the game element node, you can use it as an context to fetch other data.
$date = $xpath->evaluate('string(date)', $game);
string() casts the first node of the location path into a string. If it can not match a node, it will return an empty string. Check normalize-space() if you want to remove whitespaces at the same time.
You can validate if the game element has a date node using count().
$hasDate = $xpath->evaluate('count(date) > 0', $game);
The result of this XPath expression is always a boolean.
I am using XPath, and this is my query:
$elements = $xpath->query('//div/div/div/div/div/div[#id="con1"]/table/tr/td');
And everything works fine.
Then I change the condition in the div, and the query is like this:
$elements = $xpath->query('//div/div/div/div/div/div[#id="con2"]/table/tr/td');
And I do see what I must see.
But later, if I do this:
$elements = $xpath->query('//div/div/div/div/div/div[#id="con1" or #id="con2"]/table/tr/td');
I see again only the elements of con1. Why is that?
The full code is below:
$elements = $xpath->query('//div/div/div/div/div/div[#id="con1" or #id="con2"]/table/tr/td');
foreach ( $elements as $element ) {
$str1=$element->getAttribute('class');
$str2="first-td";
$str3="status";
if (strcmp($str1,$str2)==0) {
var_dump( $element->nodeValue);
}
if (strcmp($str1,$str3)==0) {
echo $element->childNodes->item(0)->getAttribute('class'). "<br />";
}
}
To sum up: If my condition is only con1, I see the correct results. If it's only con2, I see the correct results. The problem comes when I am using the or. In that case, I see the results only from con1. It's like it's stopping after fullfilling the first condtions. They are at the same level of the DOM tree.
What you are trying to do is to retrieve <div id="con1"> and <div id="con2"> in the same expression, but what you are actually doing is to retrieve a div which either has an attribute id="con1" or id="con2". The first expression of the condition returns true and then you get the <div id="con1"> node. It makes sense.
To get both nodes you need something like:
//div[#id="con1"]|//div[#id="con2"
Note: //div[#id="con1"] finds whatever node <div id="con1"> in the tree and the id in a document has to be unique. It's not necessary to specify all the path down.
So I have a HTML string like this:
<td class="name">
Some Name
</td>
<td class="name">
Some Name2
</td>
Using XPath I'm able to get value of href attribute using this Xpath query:
$domXpath = new \DOMXPath($this->domPage);
$hrefs = $domXpath->query("//td[#class='name']/a/#href");
foreach($hrefs as $href) {...}
And It's even easier to get a text value, like this:
// Xpath auto. strips any html tags so we are
// left with clean text value of a element
$domXpath = new \DOMXPath($this->domPage);
$names = $domXpath->query("//td[#class='name']/");
foreach($names as $name) {...}
Now I'm curious to know, how can I combine those two queries to get both values with only one query (If it's something like that even posible?).
Fetch
//td[#class='name']/a
and then pluck the text with nodeValue and the attribute with getAttribute('href').
Apart from that, you can combine Xpath queries with the Union Operator | so you can use
//td[#class='name']/a/#href|//td[#class='name']
as well.
To reduce the code to a single loop, try:
$anchors = $domXpath->query("//td[#class='name']/a");
foreach($anchors as $a)
{
print $a->nodeValue." - ".$a->getAttribute("href")."<br/>";
}
As per above :) Too slow ..
Simplest way, evaluate is for this task!
The simplest way to obtain a value is by evaluate() method:
$xp = new DOMXPath($dom);
$v = $xp->evaluate("string(/etc[1]/#stringValue)");
Note: important to limit XPath returns to 1 item (the first a in this case), and cast the value with string() or round(), etc.
So, in a set of multiple items, using your foreach code,
$names = $domXpath->query("//td[#class='name']/");
foreach($names as $contextNode) {
$text = $domXpath->evaluate("string(./a[1])",$contextNode);
$href = $domXpath->evaluate("string(./a[1]/#href)",$contextNode);
}
PS: this example is only for evaluate's illustration... When the information already exists at the node, use what offers best performance, as methods getAttribute(), saveXML(), etc. and properties as $nodeValue, $textContent, etc. supplied by DOMNode. See #Gordon's answer for this particular problem. The XPath subquery (at context) is good for complex cases — or symplify your code, avoiding to check hasChildNodes() + loop for $childNodes, etc. with no significative gain in performance.
The code below helps be to get the WHOLE XML and put it into an array. What I'm wondering is, what would be a good way to get the XML only from item 3 - 6 or any arbitrary range instead of the whole document.
$d = new DOMDocument();
$d->load('http://news.google.com/?output=rss');
foreach ($d->getElementsByTagName('item') as $t) {
$list = array ( 'title' => $t->getElementsByTagName('title')->item(0)->nodeValue);
array_push($mt_arr, $list);
}
Thanks
You can use Xpath.
You can either use DOMXpath or use the xpath method to create an Xpath query that will return the subset of nodes.
$d->xpath('/SOME/XPATH/STATEMENT');