Better way of retrieving info from XML file using XPATH - php

I've got so far a very simple class named Menu.php which contains the following:
<?php
class Menu
{
private $_dom;
private $categoryItems;
function __construct()
{
if(file_exists('Menu.xml'))
{
$this->_dom = simplexml_load_file('Menu.xml');
}
}
public function retrieveMenu($category)
{
$products = $this->_dom->xpath('/menu/category[#name="'.$category.'"]');
return $products;
}
} // end of class Menu
Pretty rudimentary, I know, is just for testing purposes.
Now, I also have a XML file like the following:
<?xml version="1.0" encoding="UTF-8" ?>
<menu>
<category name="pizza">
<item name="Tomato and Cheese">
<type>Regular</type>
<available>true</available>
<size name="Small">
<price>5.50</price>
</size>
<size name="Large">
<price>9.75</price>
</size>
</item>
</category>
<category name="pizza">
<item name="Pepperoni">
<type>Regular</type>
<available>true</available>
<size name="Small">
<price>6.85</price>
</size>
<size name="Large">
<price>10.85</price>
</size>
</item>
</category>
Which goes on with multiple products. So, the idea is to access this file through the class,
and to achieve that I'm doing the following in my index.php:
<?php
require 'Menu.php';
$menu = new Menu();
$tests = $menu->retrieveMenu('pizza');
foreach($tests as $test) {
echo $test->attributes();
echo '<br />';
foreach($test->item as $item) {
echo $item->attributes();
echo '<br />';
echo $item->type;
echo '<br />';
echo $item->available;
echo '<br />';
foreach($item->size as $price) {
echo $price->attributes();
echo '<br />';
echo $price->price;
echo '<br />';
echo '<br />';
echo $price->price;
echo '<br />';*/
}
//echo $item->size->attributes();
echo '<br /><br /><br /><br />';
}
}
Which is given me back the results I'd expect:
>pizza
>Tomato and Cheese
>Regular
>true
>Small
>5.50
>Large
>9.75
Now, my question is: Since I'm using 3 nested for loops if I'm not wrong the complexity is n^3, which is pretty awful, the original XML contains lots of products, is there a better way of accessing it? Am I doing something wrong?
By the way, YES, I MUST USE XML and XPATH

Nothing wrong with the nested loops in this case. You access and output all the sub elements on a single resource. But yes it is possible with Xpath to fetch nodes from different levels into a list.
Xpath expressions can use | to combine several location paths. So its is actually three location paths and the expression returns all of the matching nodes:
name attribute nodes from all elements: //*/#name
child elements of item except size: //item/*[not(self::size)]
price child elements of item/size: //item/size/price
This example uses DOM:
$dom = new DOMDocument();
$dom->loadXml($xml);
$xpath = new DOMXpath($dom);
$expression = '//*/#name|//item/*[not(self::size)]|//item/size/price';
foreach ($xpath->evaluate($expression) as $node) {
echo trim($node->nodeValue), "<br/>\n";
}
Output:
pizza<br/>
Tomato and Cheese<br/>
Regular<br/>
true<br/>
Small<br/>
5.50<br/>
Large<br/>
9.75<br/>
pizza<br/>
Pepperoni<br/>
Regular<br/>
true<br/>
Small<br/>
6.85<br/>
Large<br/>
10.85<br/>
A location path in Xpath works like a filter, the nodes are returned in an order depending on their position in the document.
It works with SimpleXML, too:
$element = simplexml_load_string($xml);
$expression = '//*/#name|//item/*[not(self::size)]|//item/size/price';
foreach ($element->xpath($expression) as $node) {
echo trim($node), "<br/>\n";
}
Demo: https://eval.in/156266

Related

Get xml node full path in Php / SimpleXml

I need the full path of an xml node.
I saw the answer in this question but I wasn't able to use it.
Below the code I used on a php web tester with no success:
$xml = <<<EOF
<root>
<First>
<Martha>Text01</Martha>
<Lucy>Text02</Lucy>
<Bob>
<Jhon>Text03</Jhon>
</Bob>
<Frank>One</Frank>
<Jessy>Two</Jessy>
</First>
<Second>
<Mary>
<Jhon>Text04</Jhon>
<Frank>Text05</Frank>
<Jessy>Text06</Jessy>
</Mary>
</Second>
</root>
EOF;
$MyXml = new SimpleXMLElement($xml);
$Jhons = $MyXml->xpath('//Jhon');
foreach ($Jhons as $Jhon){
echo (string) $Jhon;
//No one of the following works
echo (string) $Jhon->xpath('./node()/path()');
echo (string) $Jhon->xpath('./path()');
echo (string) $Jhon->xpath('.path()');
echo (string) $Jhon->path();
echo '<br/> ';
}
I need: "/root/First/Bob/Jhon" and "/root/Second/Mary/Jhon"
You can use the much more powerful DOM (DOMDocument based in PHP) api to do this...
$MyXml = new SimpleXMLElement($xml);
$Jhons = $MyXml->xpath('//Jhon');
foreach ($Jhons as $Jhon){
$dom = dom_import_simplexml($Jhon);
echo $dom->getNodePath().PHP_EOL;
}
The dom_import_simplexml($Jhon) converts the node and then getNodePath() displays the path...
This gives ( for the example)
/root/First/Bob/Jhon
/root/Second/Mary/Jhon
Or if you just want to stick to SimpleXML, you can use the XPath axes ancestor-or-self to list the current node and each parent node...
$MyXml = new SimpleXMLElement($xml);
$Jhons = $MyXml->xpath('//Jhon');
foreach ($Jhons as $Jhon){
$parent = $Jhon->xpath("ancestor-or-self::*");
foreach ( $parent as $p ) {
echo "/".$p->getName();
}
echo PHP_EOL;
}

PHP Array Comparison using If

I am having a hard time trying to understand why I can't compare the values of two arrays in PHP. If I echo both of these during the loop using "echo $description->ItemDesriptionName;" and "echo $item->ItemName;" the values seem to show as the same, but when I try to compare them using if, nothing works. What am I missing?
<?php
$xml=simplexml_load_file("test.xml") or die("Error: Cannot create object");
$categories = $xml->Menu->Categories;
$items = $xml->Menu->Categories->Items->ItemObject;
$itemdescription = $xml->Menu->Options->Description->DescriptionObject;
foreach($items as $item) {
echo $item->ItemName . ' - ' . $item->Price . '</br>';
foreach ($itemdescription as $description) {
if ($description->ItemDescriptionName == $item->ItemName) {
echo 'We have a match!';
//where I would echo $description->ItemDescription;
}
}
}
?>
Here is the XML file
<?xml version="1.0" encoding="utf-8"?>
<Root>
<Menu>
<Categories>
<Name>Category 1</Name>
<Items>
<ItemObject>
<ItemName>Item 1</ItemName>
<Price>1</Price>
</ItemObject>
<ItemObject>
<ItemName>Item 2</ItemName>
<Price>3</Price>
</ItemObject>
</Items>
</Categories>
<Options>
<Description>
<DescriptionObject>
<ItemDescriptionName>Item 1</ItemDescriptionName>
<ItemDescription>A Great item</ItemDescription>
</DescriptionObject>
<DescriptionObject>
<ItemDescriptionName>Item 2</ItemDescriptionName>
<ItemDescription>A Great item as well</ItemDescription>
</DescriptionObject>
</Description>
</Options>
</Menu>
</Root>
compare as string
and you have typo in ItemDescriptioName (ItemDescriptionName)
if ( (string)$description->ItemDescriptionName == (string)$item->ItemName) {
Convert to string and then compare
<?php
$xml=simplexml_load_file("test.xml") or die("Error: Cannot create object");
$menu = $xml->Menu;
$categories = $xml->Menu->Categories;
$items = $xml->Menu->Categories->Items->ItemObject;
$itemdescription = $xml->Menu->Options->Description->DescriptionObject;
foreach($items as $item) {
$itemname = $item->ItemName;
foreach ($itemdescription as $description) {
$descriptionname = $description->ItemDescriptionName ;
echo $itemname." ---- ".$descriptionname."<br/>";
if((string)$itemname === (string)$descriptionname){
echo "Yes its matched";
}
}
}
?>
Working fine for me
The properties like $description->ItemDescriptionName are SimpleXMLElement objects. So you do not compare strings but two objects.
SimpleXMLElement objects implement the magic method __toString(). They can be cast to string automatically, but a compare between to objects will not trigger that. You can force it:
if ((string)$description->ItemDescriptionName === (string)$item->ItemName) {
...
Can you access them directly instead using an accordant index?
...
$items = $xml->Menu->Categories->Items->ItemObject;
$itemdescription = $xml->Menu->Options->Description;
$i = 0;
foreach ($items as $item) {
echo $i.' '.$item->ItemName . ' - ' . $item->Price;
echo $itemdescription->DescriptionObject[$i]->ItemDescriptionName[0];
echo ' ';
echo $itemdescription->DescriptionObject[$i]->ItemDescription[0];
echo '</br>';
$i++;
}

Read colon tags values XML PHP

I've already read those topics:
PHP library for parsing XML with a colons in tag names? and
Simple XML - Dealing With Colons In Nodes but i coundt implement those solutions.
<item>
<title> TITLE </title>
<itunes:author> AUTHOR </itunes:author>
<description> TEST </description>
<itunes:subtitle> TEST </itunes:subtitle>
<itunes:summary> TEST </itunes:summary>
<itunes:image href="yoyoyoyo.jpg"/>
<pubDate> YESTERDAY </pubDate>
<itunes:block>no</itunes:block>
<itunes:explicit>no</itunes:explicit>
<itunes:duration>99:99:99</itunes:duration>
<itunes:keywords>key, words</itunes:keywords>
</item>
I want to get only itunes:duration and itunes:image. Here is my code:
$result = simplexml_load_file("http://blablabla.com/feed.xml");
$items = $result->xpath("//item");
foreach ($items as $item) {
echo $item->title;
echo $item->pubDate;
}
I tried using children() method but when i try to print_r it it says that the node no longer exists.
You should use the children() on the $item element to get it's child-elements:
$str =<<< END
<item>
<title> TITLE </title>
<itunes:author> AUTHOR </itunes:author>
<description> TEST </description>
<itunes:subtitle> TEST </itunes:subtitle>
<itunes:summary> TEST </itunes:summary>
<itunes:image href="yoyoyoyo.jpg"/>
<pubDate> YESTERDAY </pubDate>
<itunes:block>no</itunes:block>
<itunes:explicit>no</itunes:explicit>
<itunes:duration>99:99:99</itunes:duration>
<itunes:keywords>key, words</itunes:keywords>
</item>
END;
$result = #simplexml_load_string($str);
$items = $result->xpath("//item");
foreach ($items as $item) {
echo $item->title . "\n";
echo $item->pubDate . "\n";
echo $item->children()->{'itunes:duration'} . "\n";
}
Output:
TITLE
YESTERDAY
99:99:99
Here goes my alternative solution if Dekel's dont work for someone.
Using method getNamespaces
$result = simplexml_load_file("http://blablabla.com/feed.xml");
$items = $result->xpath("//item");
foreach ($items as $item)
{
$itunesSpace = $item->getNameSpaces(true);
$nodes = $item->children($itunesSpace['itunes']);
//TEST
echo $nodes->subtitle
//99:99:99
echo $nodes->duration
//If you want the image Href
$imageAux = $nodes->image->attributes();
//yoyoyoyo.jpg
echo $imageAux['href'];
}

Need to show child data on parent id

i'm struggling with Xpath, i have an xml list and i need to get the child data based on the parent id ...
My xml file :
<projecten>
<project id="1">
<titel>Shop 1</titel>
<siteurl>http://test.be</siteurl>
<screenshot>test.jpg</screenshot>
<omschrijving>comment 1</omschrijving>
</project>
<project id="2">
<titel>Shop 2</titel>
<siteurl>http://test2.be</siteurl>
<screenshot>test2.jpg</screenshot>
<omschrijving>comment</omschrijving>
</project>
</projecten>
the code i use to get for example the project 1 data (does not work):
$xmlDoc = new DOMDocument();
$xmlDoc->load(data.xml);
$xpath = new DOMXPath($xmlDoc);
$projectId = '1';
$query = '//projecten/project[#id='.$projectId.']';
$details = $xpath->query($query);
foreach( $details as $detail )
{
echo $detail->titel;
echo $detail->siteurl;
echo $detail->screenshot;
echo $detail->omschrijving;
}
But this does not show anything, if someone can point me out ... thanks
In addition to the solution already given you can also use:
foreach ($xpath->query(sprintf('/projecten/project[#id="%d"]', $id)) as $projectNode) {
echo
$projectNode->getElementsByTagName('titel')->item(0)->nodeValue,
$projectNode->getElementsByTagName('siteurl')->item(0)->nodeValue,
$projectNode->getElementsByTagName('screenshot')->item(0)->nodeValue,
$projectNode->getElementsByTagName('omschrijving')->item(0)->nodeValue;
}
or fetch the DOMText node values directly with Xpath
foreach ($xpath->query(sprintf('/projecten/project[#id="%d"]', $id)) as $projectNode) {
echo
$xpath->evaluate('string(titel)', $projectNode),
$xpath->evaluate('string(siteurl)', $projectNode),
$xpath->evaluate('string(screenshot)', $projectNode),
$xpath->evaluate('string(omschrijving)', $projectNode);
}
or import the node to SimpleXml
foreach ($xpath->query(sprintf('/projecten/project[#id="%d"]', $id)) as $projectNode) {
$detail = simplexml_import_dom($projectNode);
echo
$detail->titel,
$detail->siteurl,
$detail->screenshot,
$detail->omschrijving;
}
or even concatenate all the values directly in the XPath:
$xpath = new DOMXPath($dom);
echo $xpath->evaluate(
sprintf(
'concat(
/projecten/project[#id = %1$d]/titel,
/projecten/project[#id = %1$d]/siteurl,
/projecten/project[#id = %1$d]/screenshot,
/projecten/project[#id = %1$d]/omschrijving
', $id
)
);
Accessing the child nodes as you do:
echo $detail->title;
Is not valid, if you use DOM* functions. This would probably work if you were using SimpleXML.
For DOM* try this:
$dom = new DOMDocument;
$dom->loadXml('<projecten>
<project id="1">
<titel>Shop 1</titel>
<siteurl>http://test.be</siteurl>
<screenshot>test.jpg</screenshot>
<omschrijving>comment 1</omschrijving>
</project>
<project id="2">
<titel>Shop 2</titel>
<siteurl>http://test2.be</siteurl>
<screenshot>test2.jpg</screenshot>
<omschrijving>comment</omschrijving>
</project>
</projecten>
');
$id = 2;
$xpath = new DOMXPath($dom);
foreach ($xpath->query(sprintf('/projecten/project[#id="%s"]', $id)) as $projectNode) {
// repeat this for every needed node
$titleNode = $xpath->query('titel', $projectNode)->item(0);
if ($titleNode instanceof DOMElement) {
echo $titleNode->nodeValue;
}
// or us a loop for all child nodes
foreach ($projectNode->childNodes as $childNode) {
echo $childNode->nodeValue;
}
}

php simple xml how to read multiple nodes with different child node levels

I have an xml file that has different named nodes and multi level child nodes (that are different between each node.) How should I access the data? Will it require many nested for loops?
Here is a sample of the xml code:
<start_info>
<info tabindex="1">
<infonumber>1</infonumber>
<trees>green</trees>
</info>
</start_info>
<people>
<pe>
<people_ages>
<range number="1">
<age value="1">1</age>
<age value="2">2</age>
</range>
</people_ages>
</pe>
</people>
Here is my code so far:
$xml = simplexml_load_file("file.xml");
echo $xml->getName() . "start_info";
foreach($xml->children() as $child)
{
echo $child->getName() . ": " . $child . "<br />";
}
Here is some example code that I hope can point you in the right direction. Essentially, it is walking the DOMDocument echoing the element name and values. Note that the whitespace between the elements is significant, so for the purposes of the demo, the XML is compacted. You may find a similar issue loading from a file, so if you are not getting the expected output you might need to strip whitespace nodes.
You could replace the //root/* with a different XPath for example //people if you only wanted the <people> elements.
<?php
$xml = <<<XML
<root><start_info><info tabindex="1"><infonumber>1</infonumber><trees>green</trees></info></start_info>
<people><pe><people_ages><range number="1"><age value="1">1</age><age value="2">2</age></range></people_ages></pe></people>
</root>
XML;
$dom = new DOMDocument();
$dom->recover = true;
$dom->loadXML($xml);
$xpath = new DOMXPath($dom);
$nodelist = $xpath->query('//root/*');
foreach ($nodelist as $node) {
echo "\n$node->tagName";
getData($node);
}
function getData($node) {
foreach ($node->childNodes as $child) {
if ($child->nodeType == XML_ELEMENT_NODE) {
echo ($child->tagName === '' ? '' : "\n").$child->tagName;
}
if ($child->nodeType == XML_TEXT_NODE) {
echo '->'.$child->nodeValue;
}
if ($child->hasChildNodes()) {
getData($child); // recursive call
}
}
}
?>
check this
$xml_file = 'file.xml';
$xmlobj = simplexml_load_file($xml_file);
echo $xmlobj->getName() . 'start_info<br />';
foreach($xmlobj->children() as $childs) {
echo $childs->getName(). ': '. '<br />';
if($childs->count()>1) {
foreach($childs as $child) {
echo $child->getName(). ': '. $child. '<br />';
}
}
}

Categories