SimpleXMLElement foreach child Array/Value - php

I have the following code which works but seems like the incorrect way to implement this. Disregard the "...." that is all extra stuff we need not be concerned by. The issue I was having was that Sup was an array some of the time and other times it was just a value (or so print_r claimed, I thought/hoped it would have just been a one element array).
$users is a simpleXMLElement.
foreach($users as $user) {
if ($user->InstSup->Sup[1] == '') {
foreach($user->InstSup as $affid) {
....
} else {
foreach($user->InstSup->Sup as $affid) {
Here are the varying instances...
<Users>
<User>
<InstSup><Sup>1</Sup></InstSup>
</User>
<User>
<InstSup><Sup>2</Sup><Sup>3</Sup><Sup>4</Sup><Sup>5</Sup></InstSup>
</User>
</Users>
Thanks.

First of all, don't trust the output of print_r (or var_dump) when you deal with SimpleXMLElements. It's not showing the whole picture, better take a look at the XML as-is, for example with the asXML() method.
Now to the problem you've got. When you want to have just the list (so to speak an "array") of the <Sup> elements that are children of <InstSup>, you better query the document with Xpath. It's fairly straight forward and gives you the array you want:
$users = new SimpleXMLElement($buffer);
$sups = $users->xpath('/Users/User/InstSup/Sup');
foreach ($sups as $index => $sup) {
printf("#%d: %s (%s)\n", $index, $sup, $sup->asXML());
}
This creates the following output:
#0: 1 (<Sup>1</Sup>)
#1: 2 (<Sup>2</Sup>)
#2: 3 (<Sup>3</Sup>)
#3: 4 (<Sup>4</Sup>)
#4: 5 (<Sup>5</Sup>)
And this is the $buffer to complete the example:
$buffer = <<<XML
<Users>
<User>
<InstSup><Sup>1</Sup></InstSup>
</User>
<User>
<InstSup><Sup>2</Sup><Sup>3</Sup><Sup>4</Sup><Sup>5</Sup></InstSup>
</User>
</Users>
XML;
As the line-up in the output shows, even though the <Sup> elements are inside (same-named but) different parent elements, the XPath query expression
/Users/User/InstSup/Sup
returns all the elements in that path from the document.
So hopefully you now better understand that it's not only that print_r is not that useful because it doesn't show the whole picture, but also by understanding how the document has it's nodes ordered, you can even more easily query the data with an Xpath expression.

Related

PHP Split XML based on multiple nodes

I honestly tried to find a solution for php, but a lot of threads sound similar, but are not applicable for me or are for completely different languages.
I want to split an xml file based on nodes. Ideally multiple nodes, but of course one is enough and could be applied multiple times.
e.g. I want to split this by the tag <thingy> and <othernode>:
<root>
<stuff />
<thingy><othernode>one</othernode></thingy>
<thingy><othernode>two</othernode></thingy>
<thingy>
<othernode>three</othernode>
<othernode>four</othernode>
</thingy>
<some other data/>
</root>
Ideally I want to have 4 xmlstrings of type:
<root>
<stuff />
<thingy><othernode>CONTENT</othernode></thingy>
<some other data/>
</root>
With CONTENT being one, two, three and four. Plottwist: CONTENT can also be a whole subtree. Of course it all also can be filled with various namespaces and tag prefixes (like <q1:node/>. Formatting is irrelevant for me.
I tried SimpleXml, but it lacks the possiblity to write into dom easily
I tried DomDocument, but all what I do seems to destroy some links/relation of parent/child nodes in some way.
I tried XmlReader/Writer, but that is extremely hard to maintain and combine (at least for me).
So far my best guess is something with DomDocument, node cloning and removing everything but one node?
Interesting question.
If I get it right, it is given that <othernode> is always a child of <thingy> and the split is for each <othernode> at the place of the first <thingy> in the original document.
DOMDocument appeared useful in this case, as it allows to easily move nodes around - including all its children.
Given the split on a node-list (from getElementsByTagName()):
echo "---\n";
foreach ($split($doc->getElementsByTagName('othernode')) as $doc) {
echo $doc->saveXML(), "---\n";
}
When moving all <othernode> elements into a DOMDocumentFragement of its own while cleaning up <thingy> parent elements when emptied (unless the first anchor element) and then temporarily bring each of them back into the DOMDocument:
$split = static function (DOMNodeList $nodes): Generator {
while (($element = $nodes->item(0)) && $element instanceof DOMElement) {
$doc ??= $element->ownerDocument;
$basin ??= $doc->createDocumentFragment();
$anchor ??= $element->parentNode;
[$parent] = [$element->parentNode, $basin->appendChild($element)];
$parent->childElementCount || $parent === $anchor || $parent->parentNode->removeChild($parent);
}
if (empty($anchor)) {
return;
}
assert(isset($basin, $doc));
while ($element = $basin->childNodes->item(0)) {
$element = $anchor->appendChild($element);
yield $doc;
$anchor->removeChild($element);
}
};
This results in the following split:
---
<?xml version="1.0"?>
<root>
<stuff/>
<thingy><othernode>one</othernode></thingy>
<some other="data"/>
</root>
---
<?xml version="1.0"?>
<root>
<stuff/>
<thingy><othernode>two</othernode></thingy>
<some other="data"/>
</root>
---
<?xml version="1.0"?>
<root>
<stuff/>
<thingy><othernode>three</othernode></thingy>
<some other="data"/>
</root>
---
<?xml version="1.0"?>
<root>
<stuff/>
<thingy><othernode>four</othernode></thingy>
<some other="data"/>
</root>
---

Get all children from certain xml child element using SimpleXMLElement and xpath

I have xml like:
<root xmlns="urn:test:apis:baseComponents">
<books>
<book>
<name>50 shades of grey</name>
</book>
</books>
<disks>
<disk>
<name>Britney Spears</name>
</disk>
</disks>
</root>
And such php code:
$xml = new SimpleXMLElement($xml);
$books = $xml->books;
$disks = $xml->disks;
$disks->registerXPathNamespace('x', 'urn:test:apis:baseComponents');
$books->registerXPathNamespace('x', 'urn:test:apis:baseComponents');
$b_names = $books->xpath('//x:name');
b_names contains array with 2 values instead of 1. First holds books->book->name, second holds disks->disk->name.
Can you please explain what am I doing wrong and how could I find children of only one element?
The reason that I am using xpath instead of taking manually values using SimpleXMLElement, is that I don't know what value, which I want to search in advance.
Use $books->xpath('.//x:name') to search descendants of your $books variable and not descendants of the root node/document node (which the path //x:name does).

XML Assign More Than One Result To PHP Variable

I have done a bit of searching on this, but am just not sure I am searching for the right thing. Examples and things I have found have just confused me and possibly sent me in the wrong direction.
I am trying to figure out a php while statement, or if statement to return the results of XML output. The thing is the row/section I need may not always be the same number of results returned. For example there are ShoutCast streams, some have 1 mount point, and some have 3 mount points. Each mount point can have a different amount of listeners tuned in to that particular mount.
My Goal: To get the integer from all mount points returned in the XML, add them together to make a grand total of listeners.
The XML
<centovacast version="3.1.2" host="host.net">
<response type="success">
<message>Complete</message>
<data>
<status>
<mount>/stream</mount>
<sid>1</sid>
<listenercount>31</listenercount>
<genre>Blues</genre>
<url>http://www.websiteurl.com</url>
<title>Streams Name</title>
<currentsong>Artist Name - Track Name</currentsong>
<bitrate>128</bitrate>
<sourceconnected>1</sourceconnected>
<codec>audio/mpeg</codec>
<displayname>/stream</displayname>
<serverstate>1</serverstate>
<appstate>
<sctrans2>1</sctrans2>
</appstate>
<sourcestate>1</sourcestate>
<reseller/>
<useserver>1</useserver>
<ipaddress>11.11.111.111</ipaddress>
<port>8031</port>
<proxy>0</proxy>
<servertype>ShoutCast2</servertype>
<sourcetype>sctrans2</sourcetype>
</status>
<mountpoints>
<row>
<mount>/stream</mount>
<sid>1</sid>
<listenercount>31</listenercount>
<genre>Blues</genre>
<url>http://www.websiteurl.com</url>
<title>Stream Title Name</title>
<currentsong>Artist Name - Track Name</currentsong>
<bitrate>128</bitrate>
<sourceconnected>1</sourceconnected>
<codec>audio/mpeg</codec>
<displayname>/stream</displayname>
</row>
<row>
<mount>/live</mount>
<sid>2</sid>
<listenercount>0</listenercount>
<genre/>
<url/>
<title/>
<currentsong/>
<bitrate>0</bitrate>
<sourceconnected>0</sourceconnected>
<codec/>
<displayname>/live</displayname>
</row>
</mountpoints>
</data>
</response>
</centovacast>
So on the above I know how to pull the listeners for each mount individually using the following code.
$countlisteners->response->data->mountpoints->row[0]->listenercount;
That gives me the result for the first mount, and switching the 0 to a 1 gives me the second mount, so on and so forth.
What I need is for php that will count how many of those mounts exist, and assign each result to a variable I can then use to add together to get a grand total. Is there a way to do this?
What about doing something like this?
$countlisteners = simplexml_load_file('http://urltoxml.com');
foreach($countlisteners->response->data->mountpoints->row->listenercount as $result){
$total = $result;
echo $total;
}
You can use DOMDocument for extracting all mountpoint tags
<?php
$xml="Your xml document content here";
$dom = new DOMDocument;
$dom->loadXML($xml);
$books = $dom->getElementsByTagName('mountpoints');
foreach ($mountpoints as $mountpoints) {
echo $mountpoints->nodeValue;
//you can add your count variable here
//nodeValues can be assigned to varables
}
?>
I figured it out. So simplistic, yet hard to figure out.
$total = 0;
foreach($countlisteners->response->data->mountpoints->row as $result){
$total += $result->listenercount;
$items++;
}
echo $total;
You normally do that with Xpath. It's a query language for XML documents.
You're interested in all listenercount elements, the Xpath expression for these elements could be as simple as:
//listenercount
When you now use SimpleXML to parse the document, the following line of code gives you three SimpleXMLElements inside an array that represent the three values you want to create the sum of:
$array = simplexml_load_string($buffer)->xpath('//listenercount');
As you need the sum of the integer values of these three elements, it can be easily processed with array_map and array_sum:
$sum = array_sum(array_map('intval', $array));
And this gives you in $sum what you're looking for:
var_dump($sum); # int(62)
I hope this sheds you some light why it's often better to get the information you're looking for with an xpath query from the document instead of writing many lines of code to traverse the document "on your own".
The full example:
$buffer = <<<XML
<centovacast version="3.1.2" host="host.net">
<response type="success">
<message>Complete</message>
<data>
<status>
<mount>/stream</mount>
<sid>1</sid>
<listenercount>31</listenercount>
<genre>Blues</genre>
<url>http://www.websiteurl.com</url>
<title>Streams Name</title>
<currentsong>Artist Name - Track Name</currentsong>
<bitrate>128</bitrate>
<sourceconnected>1</sourceconnected>
<codec>audio/mpeg</codec>
<displayname>/stream</displayname>
<serverstate>1</serverstate>
<appstate>
<sctrans2>1</sctrans2>
</appstate>
<sourcestate>1</sourcestate>
<reseller/>
<useserver>1</useserver>
<ipaddress>11.11.111.111</ipaddress>
<port>8031</port>
<proxy>0</proxy>
<servertype>ShoutCast2</servertype>
<sourcetype>sctrans2</sourcetype>
</status>
<mountpoints>
<row>
<mount>/stream</mount>
<sid>1</sid>
<listenercount>31</listenercount>
<genre>Blues</genre>
<url>http://www.websiteurl.com</url>
<title>Stream Title Name</title>
<currentsong>Artist Name - Track Name</currentsong>
<bitrate>128</bitrate>
<sourceconnected>1</sourceconnected>
<codec>audio/mpeg</codec>
<displayname>/stream</displayname>
</row>
<row>
<mount>/live</mount>
<sid>2</sid>
<listenercount>0</listenercount>
<genre/>
<url/>
<title/>
<currentsong/>
<bitrate>0</bitrate>
<sourceconnected>0</sourceconnected>
<codec/>
<displayname>/live</displayname>
</row>
</mountpoints>
</data>
</response>
</centovacast>
XML;
$array = simplexml_load_string($buffer)->xpath('//listenercount');
$sum = array_sum(array_map('intval', $array));
var_dump($sum);

xml xpath does not return node value

I have a test file where I'm trying to parse an xml string using SimpleXML's xpath method.
When I try to access a nodes values directly using xpath I get empty output, but when I use xpath to grab the elements and then loop through them it works fine.
When I look at the documentation, it seems like my syntax should work. Is there something I'm missing?
<?php
$xmlstring = '<?xml version="1.0" encoding="iso-8859-1"?>
<users>
<user>
<firstname>Sheila</firstname>
<surname>Green</surname>
<address>2 Good St</address>
<city>Campbelltown</city>
<country>Australia</country>
<contact>
<phone type="mobile">1234 1234</phone>
<url>http://example.com</url>
<email>pamela#example.com</email>
</contact>
</user>
<user>
<firstname>Bruce</firstname>
<surname>Smith</surname>
<address>1 Yakka St</address>
<city>Meekatharra</city>
<country>Australia</country>
<contact>
<phone type="landline">4444 4444</phone>
<url>http://yakka.example.com</url>
<email>bruce#yakka.example.com</email>
</contact>
</user>
</users>';
// Start parsing
if(!$xml = simplexml_load_string($xmlstring)){
echo "Error loading string ";
} else {
echo "<pre>";
// Print all firstname values directly from xpath
// This outputs the elements, but the values are blank
print_r($xml->xpath("/users/user/firstname"));
// Set a variable with all of the user elements and then loop through and print firstname values
// This DOES output the values
$users = $xml->xpath("/users/user");
foreach($users as $user){
echo $user->firstname;
}
// Find all firstname values by tag
// This does not output the values
print_r($xml->xpath("//firstname"));
echo "</pre>";
}
As per the manual http://uk1.php.net/manual/en/simplexmlelement.xpath.php
The xpath method searches the SimpleXML node for children matching the XPath path.
In your first and third examples, you are being returned objects containing an array of the node's value, rather than the node itself. So you aren't going to be able to do e.g.
$results = $xml->xpath("//firstname");
foreach ($results as $result) {
echo $result->firstname;
}
Instead you can just echo out the value directly. Well, almost directly (they are still simplexml objects after all)...
$results = $xml->xpath("//firstname");
foreach ($results as $result) {
echo $result->__toString();
}

How can I normalize/check a json object output it can run through a foreach loop (php)

Maybe I just need to get some sleep, but I cannot figure this one out :( ... My problem is that the json output I have changes when it contains a single order vs multiple orders.
Here is an example json output of a single order:
{"Ack":"Success","OrderArray":{"Order":{"OrderID":"165921181012"}},"OrdersPerPage":"10","PageNumber":"1","ReturnedOrderCountActual":"1"}
Here is an example json output of multiple orders:
{"Ack":"Success","OrderArray":{"Order":[{"OrderID":"165921181012","OrderStatus":"Completed"},{"OrderID":"151330738592-1109250612005","OrderStatus":"Completed"},{"OrderID":"380931137567-501668037025","OrderStatus":"Completed"}]},"OrdersPerPage":"10","PageNumber":"1","ReturnedOrderCountActual":"3"}
The difference being the [ ]
Right now my code is as follows:
$json_o = json_decode($json);
foreach ($json_o->OrderArray->Order as $orderkey=>$o) { echo $o->OrderID; }
My first thought was to write an if statement to handle single orders Edit**, I can detect the difference between the single orders vs multiple orders using is_array / is_object. But, is there a way to add the [ ] (force a single element array) around json "OrderID" so that the foreach loop would run even if only one order exists?
If you are also generating the JSON yourself, normalise the format at that point. 'Order' should always be an array of orders, even if it only contains a single element. That, or set some other flag which signifies whether it contains only a single order or multiple orders.
If you're only consuming the JSON and have no choice:
if (!isset($json_o->OrderArray[0])) {
$json_o->OrderArray = array($json_o->OrderArray);
}
foreach ($json_O->OrderArray as $order) ...
So I found another question that dealt with a similar issue. php-convert-xml-to-json-group-when-there-is-one-child With this you can customize the json encode and add brackets where it is needed. The example below adds the array around the "status" element.
<?php
/**
* PHP convert XML to JSON group when there is one child
* #link https://stackoverflow.com/q/16935560/367456
*/
$bufferXml = <<<STRING
<?xml version="1.0" encoding="UTF-8"?>
<searchResult>
<status>
<userName1>johndoe</userName1>
</status>
<users>
<user>
<userName>johndoe</userName>
</user>
<user>
<userName>johndoe1</userName>
<fullName>John Doe</fullName>
</user>
<user>
<userName>johndoe2</userName>
</user>
<user>
<userName>johndoe3</userName>
<fullName>John Doe Mother</fullName>
</user>
<user>
<userName>johndoe4</userName>
</user>
</users>
</searchResult>
STRING;
class XML2JsonSearchResult extends SimpleXMLElement implements JsonSerializable
{
public function jsonSerialize()
{
$name = $this->getName();
if ($name == 'status') {
$value = (array)$this;
return [$value];
}
return $this;
}
}
$converter = new XML2JsonSearchResult($bufferXml);
echo "<pre>";
echo json_encode($converter, JSON_PRETTY_PRINT);
echo "</pre>";

Categories