Simplexml Object Node Iteration - php

I have an XML file that I'm parsing with PHP's Simplexml, but I'm having an issue with an iteration through nodes.
The XML:
<channel>
<item>
<title>Title1</title>
<category>Cat1</category>
</item>
<item>
<title>Title2</title>
<category>Cat1</category>
</item>
<item>
<title>Title3</title>
<category>Cat2</category>
</item>
</channel>
My counting function:
public function cat_count($cat) {
$count = 0;
$items = $this->xml->channel->item;
$size = count($items);
for ($i=0; $i<$size; $i++) {
if ($items[$i]->category == $cat) {
$count++;
}
}
return $count;
}
Am I overlooking an error in my code, or is there another preferred method for iterating through the nodes? I've also used a foreach and while statement with no luck, so I'm at a loss. Any suggestions?
EDIT: while using the xpath method below, I noticed that using
foreach ($this->xml->channel->item as $item) {
echo $item->category;
}
will print all the category name, but, using
foreach ($this->xml->channel->item as $item) {
if ($item->category == $cat) {
echo $item->category;
}
}
will only print one instance of the doubled categories. Even when I have copy and pasted the lines, only one shows. Does this mean the XML structure could be invalid somehow?

An easy way to count elements with a given name in an XML file is to use xpath. Try this:
private function categoryCount($categoryName) {
$categoryName = $this->sanitize($categoryName); // easy xpath injection protection
return count($this->xml->xpath("//item[category='$categoryName']"));
}
The sanitize() function should remove single and double quotes in your $categoryName to prevent xpath injection. To also get queries for a category name containing quotes to work, you need to build your xpath query string depending on wheather it contains single or double quotes:
// xpath in case of single quotes in category name
$xpath = '//item[category="' . $categoryName . '"]';
// xpath in case of double quotes in category name
$xpath = "//item[category='" . $categoryName . "']";
If you don't have full control over the xml data (for example if is created out of user generated content), you should take this into account. Unfortunately there is no simple way to this in php like parametrized queries.
see here for the php xpath function docs: http://php.net/manual/en/simplexmlelement.xpath.php
see here for an xpath reference: http://www.w3schools.com/xpath/xpath_syntax.asp

Related

Query XML File using PHP for Values

I am currently working on a project that requires me to query an XML file like php to return a value that matches the request. Take a look at the XML:
<ENVELOPE>
<MASTER>
<STKDETNAME>004-011</STKDETNAME>
<STKPNO>PTN771</STKPNO>
<STKPRICE></STKPRICE>
<STKOPBAL>500</STKOPBAL>
</MASTER>
<MASTER>
<STKDETNAME>004-012</STKDETNAME>
<STKPNO>PTN772</STKPNO>
<STKPRICE></STKPRICE>
<STKOPBAL>500</STKOPBAL>
</MASTER>
<MASTER>
<STKDETNAME>004-013</STKDETNAME>
<STKPNO>PTN773</STKPNO>
<STKPRICE></STKPRICE>
<STKOPBAL>1000</STKOPBAL>
</MASTER>
<MASTER>
<STKDETNAME>004-014</STKDETNAME>
<STKPNO>PTN774</STKPNO>
<STKPRICE></STKPRICE>
<STKOPBAL>1000</STKOPBAL>
</MASTER>
<MASTER>
<STKDETNAME>004-015</STKDETNAME>
<STKPNO>PTN775</STKPNO>
<STKPRICE>400</STKPRICE>
<STKOPBAL>1000</STKOPBAL>
</MASTER>
</ENVELOPE>
Now, I want to get the STKPRICE AND STKOPBAL for a SKTPNO= PTN773. This is what i have seen so far, but i don't know how to get the two values. I am new to XML.
$file = 'stocksum.xml';//same file as above
$xmlfile = simplexml_load_file($file);
$partno = PTN775;
$fnd = $xmlfile->xpath('/ENVELOPE/MASTER/STKPNO[.="$partno"]');
There are a couple of issues with the code which are just syntax problems, these are the partno needing quotes and when building the XPath expression, you use single quotes so it doesn't insert the actual part number.
BUT to get to your actual problem, if you change your XPath to the one used here, this will find the <MASTER> element whose <STKPNO> is the one your after. So then you can refer to the elements withing the <MASTER> element using standard SimpleXML object notation...
$partno = 'PTN775';
$fnd = $xmlfile->xpath('/ENVELOPE/MASTER[STKPNO="'.$partno.'"]');
echo $fnd[0]->STKPRICE.PHP_EOL;
Note that as xpath() returns a list of matches, I use $fnd[0] to get the first one.
Code which also has a check to see if the part actually exists...
$xmlfile = simplexml_load_file($file);
$partno = 'PTN7751';
$fnd = $xmlfile->xpath('/ENVELOPE/MASTER[STKPNO="'.$partno.'"]');
if ( count($fnd) == 0 ) {
echo "Not found";
}
else {
echo $fnd[0]->STKPRICE.PHP_EOL;
}

Filter php simple_xml load results with xpath

I need a little help to figure out XPath search inside XML output simple_xml_load in PHP. I have this XML:
<PRODUCTS>
<PRODUCT>
<CODE>5009444</CODE>
<NAME>Prova</NAME>
<IMG>prova.jpg</IMG>
</PRODUCT>
....
....
</PRODUCTS>
I want to filter and iterate through this data to find and return all occurrences with a variable code.
I used this syntax but didn't work
$id = 1;
$struct = \App\Models\Structures::where('id', $id)->first();
$url = 'http://demo.villaggissimi.it/public/xml/CMP_' . $struct->operators->pk .'.xml';
$xc = simplexml_load_file($url) or die("Non sono stati trovati risultati");
$xml2 = $xc->xpath("/PRODUCTS/PRODUCT[CODE='$struct->code']");
return response()->json($xml2);
The result of the Xpath Expression is a node list, SimpleXMLElement:xpath() only supports that kind of result, it will always return an array of SimpleXMLElement objects (for a valid Xpath expression).
So you still have to use foreach()/if() to avoid an error message if no element has been found.
You can however limit the result to a list with only a single node.
foreach ($xc->xpath("/PRODUCTS/PRODUCT[CODE='$struct->code'][1]") as $productNode) {
return response()->json($productNode);
}
return FALSE;
$productNodes = $xc->xpath("/PRODUCTS/PRODUCT[CODE='$struct->code'][1]");
if (count($productNodes) > 0) {
return response()->json($productNodes[0]);
}
return FALSE;

Check if child exists? - SimpleXML (PHP)

I have different XML files where I renamed for each XML file all individual tags, so that every XML file has the same tag name. That was easy because the function was customized for the XML file.
But instand of writing 7 new functions for each XML file now I want to check if a XML file has a specidifed child or not. Because if I want to say:
foreach ($items as $item) {
$node = dom_import_simplexml($item);
$title = $node->getElementsByTagName('title')->item(0)->textContent;
$price = $node->getElementsByTagName('price')->item(0)->textContent;
$url = $node->getElementsByTagName('url')->item(0)->textContent;
$publisher = $node->getElementsByTagName('publisher')->item(0)->textContent;
$category = $node->getElementsByTagName('category')->item(0)->textContent;
$platform = $node->getElementsByTagName('platform')->item(0)->textContent;
}
I get sometimes: PHP Notice: Trying to get property of non-object in ...
For example. Two different XML sheets. One contains publisher, category and platform, the other not:
XML 1:
<products>
<product>
<desc>This is a Test</desc>
<price>11.69</price>
<price_base>12.99</price_base>
<publisher>Stackoverflow</publisher>
<category>PHP</category>
</packshot>
<title>Check if child exists? - SimpleXML (PHP)</title>
<url>http://stackoverflow.com/questions/ask</url>
</product>
</products>
XML 2:
<products>
<product>
<image></image>
<title>Questions</title>
<price>23,90</price>
<url>google.de/url>
<platform>Stackoverflow</platform>
</product>
</products>
You see, sometimes one XML file contains publisher, category and platform but sometimes not. But it could also be that not every node of a XML file contains all attributes like in the first!
So I need to check for every node of a XML file individual if the node is containing publisher, category or/and platform.
How can I do that with SimpleXML?
I thought about switch case but at first I need to check which childs are contained in every node.
EDIT:
Maybe I found a solution. Is that a solution or not?
if($node->getElementsByTagName('platform')->item(0)){
echo $node->getElementsByTagName('platform')->item(0)->textContent . "\n";
}
Greetings and Thank You!
One way to rome... (working example)
$xml = "<products>
<product>
<desc>This is a Test</desc>
<price>11.69</price>
<price_base>12.99</price_base>
<publisher>Stackoverflow</publisher>
<category>PHP</category>
<title>Check if child exists? - SimpleXML (PHP)</title>
<url>http://stackoverflow.com/questions/ask</url>
</product>
</products>";
$xml = simplexml_load_string($xml);
#set fields to look for
foreach(['desc','title','price','publisher','category','platform','image','whatever'] as $path){
#get the first node
$result = $xml->xpath("product/{$path}[1]");
#validate and set
$coll[$path] = $result?(string)$result[0]:null;
#if you need here a local variable do (2 x $)
${$path} = $coll[$path];
}
#here i do array_filter() to remove all NULL entries
print_r(array_filter($coll));
#if local variables needed do
extract($coll);#this creates $desc, $price
Note </packshot> is an invalid node, removed here.
xpath syntax https://www.w3schools.com/xmL/xpath_syntax.asp
Firstly, you're over-complicating your code by switching from SimpleXML to DOM with dom_import_simplexml. The things you're doing with DOM can be done in much shorter code with SimpleXML.
Instead of this:
$node = dom_import_simplexml($item);
$title = $node->getElementsByTagName('title')->item(0)->textContent;
you can just use:
$title = (string)$item->title[0];
or even just:
$title = (string)$item->title;
To understand why this works, take a look at the SimpleXML examples in the manual.
Armed with that knowledge, you'll be amazed at how simple it is to see if a child exists or not:
if ( isset($item->title) ) {
$title = (string)$item->title;
} else {
echo "There is no title!";
}

How to iterate through an XML element node with dynamic children

I currently have the following XML structure:
<root>
<maininfo>
<node>
<tournament_id>3100423</tournament_id>
<games>
<a_0>
<id>23523636</id>
<type>
<choice_4>
<choice_id>345</choice_id>
<choice_4>
<choice_9>
<choice_id>345</choice_id>
<choice_9>
... etc
</type>
</a_0>
<a_1></a_1>
<a_2></a_2>
...etc
</games>
</info>
</node>
</root>
I can easily get the id of the first node element "a_0" by just doing:
maininfo[0]->a_3130432[0]->games[0]->a_1[0]->id;
My issue is:
How do I automatically iterate (with a foreach) through all a_0, a_1, a_2 and get the values of each of these node elements and all of their children like "345" in <choice_id>345</choice_id>?
The ending numbers of a_0, a_1 + the children of choice_4, choice_9, are dynamically created and there are no logic in the _[number] counting up with +1 for each next element.
As it has been outlined previously on Stackoverflow (for example in Read XML dynamic PHP) and as well generally in the PHP manual (for example in Basic SimpleXML usage), you can iterate over all child elements by using foreach.
For example to go over all a_* elements, it's just
foreach ($xml->maininfo->node->games[0] as $name => $a) {
echo $name, "\n";
}
Output:
a_0
a_1
a_2
You then want to iterate over these their ->type children again. This is possible in pure PHP by putting one foreach into a another:
foreach ($xml->maininfo->node->games[0] as $name => $a) {
echo $name, "\n";
if (!$a->type[0]) {
continue;
}
foreach ($a->type[0] as $name => $choice) {
echo ' +- ', $name, "\n";
}
}
This now outputs:
a_0
+- choice_4
+- choice_9
a_1
a_2
This starts to get a bit complicated. As you can imagine since XML is famous for it's tree structures, you're not the first one running into this problem. Therefore a query-language to get elements from an XML document has been invented: Xpath.
With Xpath you can access XML data as if it was a file-system. As I know that each a_* element is a child of games and each choice_* element a child of type, it's pretty straight forward:
/*/maininfo/node/games/*/type/*
^ ^ ^
| | choice_*
root |
a_*
In PHP Simplexml this looks like:
$choices = $xml->xpath('/*/maininfo/node/games/*/type/*');
foreach ($choices as $choice) {
echo $choice->getName(), ': ', $choice->choice_id, "\n";
}
Output:
choice_4: 345
choice_9: 345
As this example shows, the data is now retrieved with a single foreach.
If you as well need access to the <a_*> elements, you need to have multiple foreach's or your own iteration but that is even a more advanced topic which I'd say would extend over the limits of your question.
I hope this is helpful so far. See as well SimpleXMLElement::children() which also gives all children (like ->games[0] in the first example). All example codes are as well available as a working, interactive online-demo.
If I understand it well, you can do something like:
for($i = 0; $i < $max; ++$i){
$a = $parentNode->{'a_'.$i};
}
You can do this very easily using SimpleXML :
<?php
$xmlStr = "<?xml version='1.0' standalone='yes'?>
<root>
<maininfo>
<node>
<tournament_id>3100423</tournament_id>
<games>
<a_0>
<id>23523636</id>
<type>
<choice_4>
<choice_id>345</choice_id>
</choice_4>
<choice_9>
<choice_id>345</choice_id>
</choice_9>
</type>
</a_0>
<a_1></a_1>
<a_2></a_2>
</games>
</node>
</maininfo>
</root>";
$xmlRoot = new SimpleXMLElement($xmlStr);
$i = 0;
foreach($xmlRoot->maininfo[0]->node[0]->games[0] as $a_x)
{
echo $i++ . " - " . htmlentities($a_x->asXML()) . "<br/>";
}
?>
I have modified some parts of your XML string to make it syntactically correct. You can view the results at http://phpfiddle.org/main/code/56q-san

How does one use SimpleDOM sortedXPath to sort on node value?

XML newbie here!
I have a file containing only the following XML:
<tags>
<tag>orange</tag>
<tag>apple</tag>
<tag>banana</tag>
</tags>
I want to ouput the tags alphabetically.
I am trying to use SimpleDOM library and its sortedXPath method. Here's what I have so far, which outputs the tags unsorted.
$allTags = simpledom_load_file("tags.xml");
foreach ($allTags->sortedXPath("//tags/tag", "tag") as $i => $item)
{
echo($item);
}
Could someone tell me how to write this correctly so it works? Cheers!
In XPath, you can refer the current node (called "context node") using a single dot . so if you're accessing //tags/tag you have to use . to get the value of tag. Your example becomes:
$allTags = simpledom_load_file("tags.xml");
foreach ($allTags->sortedXPath("//tags/tag", ".") as $i => $item)
{
echo($item);
}

Categories