Simplexml with multiple tags - php

In all the examples of simplexml I have seen the structure of the xml is like:
<examples>
<example>
</example>
<example>
</example>
<example>
</example>
</examples>
However I am dealing with xml in the form:
<examples>
<example>
</example>
<example>
</example>
<example>
</example>
</examples>
<app>
<appdata>
<error>
<Details>
<ErrorCode>101</ErrorCode>
<ErrorDescription>Invalid Username and Password</ErrorDescription>
<ErrorSeverity>3</ErrorSeverity>
<ErrorSource />
<ErrorDetails />
</Details>
</error>
<items>
<item>
</item>
<item>
</item>
</items>
</appdata>
</app>
I would like to skip the examples stuff, and go straight to the app tag and check if the error errorcode exists and if it doesn't, go to the items array and loop through it.
My current way of handling this is:
$items = new SimpleXMLElement($xml_response);
foreach($items as $item){
//in here I check the presence of the object properties
}
Is there a better way? The problem is the xml structure sometimes changes order so I want to be able to go straight to particular parts of the xml.

This kind of thing is very easy using XPath, and handily, SimpleXML has an xpath function built into it! XPaths allow you to select nodes in a graph based on their ancestors, descendants, attributes, values, and so on.
Here is an example of using SimpleXML's xpath function to extract data from your XML. Note that I added an extra parent element to the sample you posted so that the XML would validate.
$sxo = new SimpleXMLElement($xml);
# this selects all 'error' elements with parent 'appdata', which has parent 'app'
$error = $sxo->xpath('//app/appdata/error');
if ($error) {
# go through the error elements...
while(list( , $node) = each($error)) {
# get the error details
echo "Found an error!" . PHP_EOL;
echo $node->Details->ErrorCode
. ", severity " . $node->Details->ErrorSeverity
. ": " . $node->Details->ErrorDescription . PHP_EOL;
}
}
Output:
Found an error!
101, severity 3: Invalid Username and Password
Here's another example -- I edited the XML excerpt slightly to show the results better here:
// edited <items> section of the XML you posted:
<items>
<item>Item One
</item>
<item>Item Two
</item>
</items>
# this selects all 'item' elements under appdata/items:
$items = $sxo->xpath('//appdata/items/item');
foreach ($items as $i) {
echo "Found item; value: " . $i . PHP_EOL;
}
Output:
Found item; value: Item One
Found item; value: Item Two
There's more information in the SimpleXML XPath documentation, and try the zvon.org XPath tutorials -- they give a good grounding in XPath 1.0 syntax.

Related

PHP - Access Item from XML with xpath()

I want to get the ISBN number of a book from an Amazon XML File. I already checked other posts and tried to solve the problem with them but without any success. The $xml looks like:
<itemlookupresponse>
<items>
<item>
<itemattributes>
<studio>Pottermore from J.K. Rowling</studio>
<eisbn>9781781100769</eisbn>
</itemattributes>
</item>
<item>
<itemattributes>
<studio>Carlsen Verlag GmbH</studio>
<isbn>3551551677</isbn>
</itemattributes>
</item>
<item>
<itemattributes>
<studio>Carlsen</studio>
<isbn>3551551677</isbn>
</itemattributes>
</item>
</items>
</itemlookupresponse>
I want to get the items, where the ISBN equals 3551551677. For that reason, I used the following command, which unfortunately returns an empty array.
$item = $xml->xpath('//items/item[itemattributes/isbn=3551551677]');
I would be glad, if someone could help me and explain, what I made wrong.
XML and XPath are case-sensitive. Amazon's XML is not all lowercase, despite what you've posted in your question. Adjust your XPath to match the exact case used in Amazon's actual XML.
Note also that if there are namespaces involved in the actual XML, those too must be accounted for in your XPath. See How does XPath deal with XML namespaces?
You didn't show something important (namespaces?) because this code works for me:
$string = <<<XML
<itemlookupresponse>
<items>
<item>
<itemattributes>
<studio>Pottermore from J.K. Rowling</studio>
<eisbn>9781781100769</eisbn>
</itemattributes>
</item>
<item>
<itemattributes>
<studio>Carlsen Verlag GmbH</studio>
<isbn>3551551677</isbn>
</itemattributes>
</item>
<item>
<itemattributes>
<studio>Carlsen</studio>
<isbn>3551551677</isbn>
</itemattributes>
</item>
</items>
</itemlookupresponse>
XML;
$xml = new SimpleXMLElement($string);
/* Поиск <a><b><c> */
$items = $xml->xpath('//items/item[ ./itemattributes/isbn[.="3551551677"] ]');
//$items = $xml->xpath('//items/item[itemattributes/isbn=3551551677]');
echo "Result size: " . count($items) . "\n";
foreach ( $items as $item) {
echo $item->asXML() . "\n";
}

parsing XML file using PHP (cs-s75: David Malan's)

I am making a PHP project for a Pizza Shop [This is project-0 in David Malan's course CS-S75 Building Dynamic Websites]. And the Code that I have to write must be eXtensible. That is, if the pizza shop's owner wants to add a new category, he should be able to do that pretty easily and my PHP code must accommodate those changes in the XML file without writing any new code.
For my code to be extensible though, I need some methods for filtering the XML data.
For instance inside the root node <menu>, I have child nodes item that have attributes like
<item name="Pizzas">
<category name="Onions">
</category>
</item>
<item name="Salads">
<category name = "Garden">
</category>
</item>
and there are ten item tags in total.
What I want to do is this: if the user wants to purchase the salads, I would want to filter the XML DOM tree the following way:
// $_POST['selected'] has a value of 'Salads' stored in it
$selected = $_POST['selected']
$dom = simple_xml_loadfile("menu.xml")
foreach ($dom -> xpath("menu/item[#name = $selected ]" as $item))
{
echo $item -> category['name'].'<br />';
}
And it should print Garden and any other item that is subsequently added to the Salads category.The problem occurs with the menu/item[#name = $selected ] because this is probably not a proper method for comparing the attribute (Note that attribute comparison like this in XML requires single equal sign and not double equal).And obviously menu/item[#name = $_POST['selected']] doesn't work either.
What works is #name = "Salads" and of course this kills the whole purpose of the extensiblity of XML and dynamism of PHP.
Please help!
Let's get all category nodes that belong to a parent node that has a name attribute of your choosing:
Also note that the function name is simplexml_load_file and not simple_xml_loadfile
foreach ($dom->xpath('item[#name="' . $selected . '"]/category') as $item)
{
echo $item->attributes()->{'name'}. PHP_EOL;
}
Also note the usage of single vs. double quotes to enclose the attribute value.
For reference, this is the xml structure I used for testing:
<menu>
<item name="Pizzas">
<category name="Onions"></category>
</item>
<item name="Salads">
<category name = "Garden"></category>
<category name = "Cesar"></category>
<category name = "Onion and Tomato"></category>
</item>
</menu>

Merge two variable SimpleXML Objects in PHP

I've been trying to merge two XML files I use to build my menubar in my web application for hours, but I can't get it to work.
I have my main XML file which looks like this:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<root>
<version>1.0.0</version>
<menu>
<Category1>
<item>
<id>Cake</id>
<nr>1</nr>
<hint>I like these</hint>
<userlevel>5</userlevel>
</item>
<item>
<id>Cake 2</id>
<nr>2</nr>
<hint>I like these too, but only for me</hint>
<userlevel>10</userlevel>
</item>
<Category1>
<Category2WithApples>
<item>
<id>Apple Cake</id>
<nr>1</nr>
<hint>Sweet</hint>
<userlevel>5</userlevel>
</item>
<item>
<id>Rainbow Cake</id>
<nr>2</nr>
<hint>Mine!!</hint>
<userlevel>10</userlevel>
</item>
<Category2WithApples>
</menu>
</root>
Now, I want each user to be able to load in his custom XML which is in the same folder as the main.xml which looks like this:
<CategoryMyOwn>
<item>
<id>Item in my Category</id>
<nr>0</nr>
<hint>Some text</hint>
<userlevel>0</userlevel>
</item>
</CategoryMyOwn>
<Category1>
<item>
<id>Item in existing category</id>
<nr>0</nr>
<hint>Some text</hint>
<userlevel>0</userlevel>
</item>
</Category1>
I've tried solutions from
http://php.net/manual/de/ref.simplexml.php
php recursion, function doesn't return any value
http://durgagupta.com.np/php-how-to-merge-two-simplexml-objects/
but they all do not work at all for me or just append the second file to the end of my main.xml. So, my question is, how do I properly merge the user.xml into my main.xml so it looks like this:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<root>
<version>1.0.0</version>
<menu>
<Category1>
<item>
<id>Cake</id>
<nr>1</nr>
<hint>I like these</hint>
<userlevel>5</userlevel>
</item>
<item>
<id>Cake 2</id>
<nr>2</nr>
<hint>I like these too, but only for me</hint>
<userlevel>10</userlevel>
</item>
<item>
<id>Item in existing category</id>
<nr>0</nr>
<hint>Some text</hint>
<userlevel>0</userlevel>
</item>
<Category1>
<Category2WithApples>
<item>
<id>Apple Cake</id>
<nr>1</nr>
<hint>Sweet</hint>
<userlevel>5</userlevel>
</item>
<item>
<id>Rainbow Cake</id>
<nr>2</nr>
<hint>Mine!!</hint>
<userlevel>10</userlevel>
</item>
<Category2WithApples>
<CategoryMyOwn>
<item>
<id>Item in my Category</id>
<nr>0</nr>
<hint>Some text</hint>
<userlevel>0</userlevel>
</item>
</CategoryMyOwn>
</menu>
</root>
Your second XML is not a document, XML documents need to have a document element node. In other words here at the top level only a single element node is allowed. All other element nodes have to be descendants of that node.
You can treat this as an XML fragment however. A fragment is the inner XML of an element node.
In both cases it easier to use DOM for that.
Append a fragment to a parent element node
Let's keep it simple for the first step and append the fragment to the menu node.
$document = new DOMDocument();
$document->loadXml($targetXml);
$xpath = new DOMXpath($document);
$fragment = $document->createDocumentFragment();
$fragment->appendXml($fragmentXml);
foreach ($xpath->evaluate('/root/menu[1]') as $menu) {
$menu->appendChild($fragment);
}
echo $document->saveXml();
The Xpath expression can /root/menu[1] selects the first menu element node inside the root. This can be only one node or none.
A document fragment in DOM is a node object and can be appended like any other node (element, text, ...).
Merging nodes
Merging the category nodes is a little more difficult. But Xpath will help.
$document = new DOMDocument();
$document->loadXml($targetXml);
$xpath = new DOMXpath($document);
$fragment = $document->createDocumentFragment();
$fragment->appendXml($fragmentXml);
$menu = $xpath->evaluate("/root/menu[1]")->item(0);
foreach ($xpath->evaluate('*', $fragment) as $category) {
$targets = $xpath->evaluate("{$category->nodeName}[1]", $menu);
if ($targets->length > 0) {
$targetCategory = $targets->item(0);
foreach ($category->childNodes as $item) {
$targetCategory->appendChild($item);
}
} else {
$menu->appendChild($category);
}
}
echo $document->saveXml();
Fetching the menu node
$menu = $xpath->evaluate("/root/menu[1]")->item(0);
This is about the same like in the first simple example. It fetch the menu nodes in root and returns the first found node. You should check if the list contained a node. But for this example just take it for guaranteed.
Iterating the fragment
foreach ($xpath->evaluate('*', $fragment) as $category) {
...
}
* is a simple Xpath expression that returns any element child node. The fragment can contain other nodes (whitespace, text, comment, ...). The second argument for DOMXpath::evaluate() is the context for the Xpath expression.
Fetching the target category
Next you need to fetch the category node with the same name from the target document. This will return a list with one node or an empty list.
$targets = $xpath->evaluate("{$category->nodeName}[1]", $menu);
if ($targets->length > 0) {
...
} else {
...
}
Append to the found target category
If the category exists append all child nodes from the category in the fragment to the target.
$targetCategory = $targets->item(0);
foreach ($category->childNodes as $item) {
$targetCategory->appendChild($item);
}
Append a category
$menu->appendChild($category);
If the category doesn't exists, just append it to the menu.

xml parsing loop with a special condition

I want to loop through an xml file using simplephp.
My code for accessing is something like this:
// get the path of the file
$file = "xml/".$item_name . "_cfg.xml";
if( ! $xml = simplexml_load_file($file) ){
echo 'unable to load XML file';
} else {
$item_array = array();
foreach($xml->config->exported->item as $items)
{
$item_name = (string) $items->attributes()->name;
echo "item name: " . $item_name . "<br />";
}
That will echo out the name of all the item names in this xml, this isnt the actual xml as some of the data is sensitive but its basically the same with different data.
So it will show as the following based on the xml below:
yellow
blue
orange
red
black
here is the xml
<?xml version="1.0"?>
<main>
<config>
<exported>
<items>
<item name="yellow"></item>
<item name="blue"></item>
<New_Line />
<item name="orange"></item>
<item name="red"></item>
<New_Line />
<item name="black"></item>
</items>
</exported>
</config>
<main>
That is good but what i need to display is:
yellow
blue
--------
orange
red
--------
black
If you notice in the xml there is this line in between some of the stats
<New_Line />
And when i encounter that i want to echo a few dashes but i am not really sure how you do it as I'm not familiar with simplexml
Arguably this is a poor choice of structure in the XML, since what is presumably actually meant is that there are multiple sets of items, which should therefore have some parent to represent each individual group. Nonetheless, what you want to do is pretty easy using SimpleXML.
The trick is to use the ->children() method to iterate over all child nodes in order, regardless of their name. Then within that loop, you can examine the name of each node using ->getName() and decide how to act.
Here's an example (and a live demo of it in action); note that I've added the ->items to match the sample XML you gave, and used the shorter $node['name'] rather than $node->attributes()->name.
foreach($xml->config->exported->items->children() as $node)
{
switch ( $node->getName() )
{
case 'item':
$item_name = (string)$node['name'];
echo "item name: " . $item_name . "<br />";
break;
case 'New_Line':
echo '<hr />';
break;
}
}

Hide XML declaration in files generated using PHP

I was tesing with a simple example of how to display XML in browser using PHP and found this example which works good
<?php
$xml = new DOMDocument("1.0");
$root = $xml->createElement("data");
$xml->appendChild($root);
$id = $xml->createElement("id");
$idText = $xml->createTextNode('1');
$id->appendChild($idText);
$title = $xml->createElement("title");
$titleText = $xml->createTextNode('Valid');
$title->appendChild($titleText);
$book = $xml->createElement("book");
$book->appendChild($id);
$book->appendChild($title);
$root->appendChild($book);
$xml->formatOutput = true;
echo "<xmp>". $xml->saveXML() ."</xmp>";
$xml->save("mybooks.xml") or die("Error");
?>
It produces the following output:
<?xml version="1.0"?>
<data>
<book>
<id>1</id>
<title>Valid</title>
</book>
</data>
Now I have got two questions regarding how the output should look like.
The first line in the xml file '', should not be displayed, that is it should be hidden
How can I display the TextNode in the next line. In total I am exepecting an output in this fashion
<data>
<book>
<id>1</id>
<title>
Valid
</title>
</book>
</data>
Is that possible to get the desired output, if so how can I accomplish that.
Thanks
To skip the XML declaration you can use the result of saveXML on the root node:
$xml_content = $xml->saveXML($root);
file_put_contents("mybooks.xml", $xml_content) or die("cannot save XML");
Please note that saveXML(node) has a different output from saveXML().
First question:
here is my post where all usable threads with answers are listed: How do you exclude the XML prolog from output?
Second question:
I don't know of any PHP function that outputs text nodes like that.
You could:
read xml using DomDocument and save each node as string
iterate trough nodes
detect text nodes and add new lines to xml string manually
At the end you would have the same XML with text node values in new line:
<node>
some text data
</node>

Categories