Complex Loop through a complex SimpleXMLElement - php

I need to save some values from XML.
First step - I get the structure:
$xml = $dom_xml->saveXML();
$xml_ = new \SimpleXMLElement($xml);
dd($xml_);
Here TextFrame has 8 arrays. Each of them has PathPointType, which has
4 more arrays with 3 attributes each. And these attributes I need from each TextFrame.
I can get, for instance, Anchor value doing this:
$res = $xml_
->Spread
->TextFrame
->Properties
->PathGeometry
->GeometryPathType
->PathPointArray
->PathPointType
->attributes();
dd($res['Anchor']);
(BTW: is there more prettier way to get it?)
But the question is - how is it possible to loop through all arrays and save values separately for each array?
I assume here has to be a multidimensional foreach loop in conjunction with for loop?
Or is better to achieve it using DOMDocument?

As it looks as though you are starting off with DOMDocument (as you are using $dom_xml->saveXML() to generate the XML), it may be easier to continue using it and it also has some easy features for getting the details your after.
Using getElementsByTagName() allows you to get a list of the elements with a specific tag name from a start point, so starting with $dom_xml, get all of the <TextFrame> elements. Then foreach() over this list and using this element as a start point, use getElementsByTagName("PathPointType") to get the nested <PathPointType> elements. At this point you can then use getAttribute("Anchor") for each of the attributes you need from the <PathPointType> elements...
$textFrames = $dom_xml->getElementsByTagName("TextFrame");
foreach ( $textFrames as $frame ) {
$pathPointTypes = $frame->getElementsByTagName("PathPointType");
foreach ( $pathPointTypes as $type ) {
echo $type->getAttribute("Anchor").PHP_EOL;
}
}
Edit
You can extend the code to build an array of frames and then the anchors within that. This code also stores the anchor in an associative array so that if you add the other attributes, you can add them here (or remove it if you don't need another layer of detail)...
$frames =[];
foreach ( $textFrames as $frame ) {
$anchors = [];
$pathPointTypes = $frame->getElementsByTagName("PathPointType");
foreach ( $pathPointTypes as $type ) {
$anchors[] = ['Anchor' => $type->getAttribute("Anchor")];
}
$frames[] = $anchors;
}
Also if you have some way of identifying the frames, you could create an associative array at that level as well...
$frames[$frameID] = $anchors;

As a complement to the existing answer from Nigel Ren, I thought I'd show how the same loops look with SimpleXML.
Firstly, note that you don't need to convert the XML to string and back if you want to switch between DOM and SimpleXML for any reason, you can use simplexml_import_dom which just swaps out the interface:
$sxml = simplexml_import_dom($dom_xml);
Next we need our TextFrame elements; we could either step through the structure explicitly, as you had before:
$textFrames = $sxml->Spread->TextFrame;
Or we could use XPath to search for matching tag names within our current node (. is the current element, and // means "any descendant":
$textFrames = $sxml->xpath('.//TextFrame');
The first will give you a SimpleXMLElement object, and the second an array, but either way, you can use foreach to go through the matches.
This time we definitely want an XPath expression to get the PathPointType nodes, to avoid all the nested loops through levels we're not that interested in:
foreach ( $textFrames as $frame ) {
$pathPointTypes = $frame->xpath('.//PathPointType');
foreach ( $pathPointTypes as $type ) {
echo $type['Anchor'] . PHP_EOL;
}
}
Note that you don't need to call $type->attributes(); unless you're dealing with namespaces, all you need to get an attribute is $node['AttributeName']. Beware that attributes in SimpleXML are objects though, so you'll often want to force them to be strings with (string)$node['AttributeName'].
To take the final example, you might then have something like this:
$frames = [];
foreach ( $sxml->Spread->TextFrame as $frame ) {
$anchors = [];
$pathPointTypes = $frame->xpath('.//PathPointType');
foreach ( $pathPointTypes as $type ) {
$anchors[] = ['Anchor' => (string)$type['Anchor']];
}
$frames[] = $anchors;
}

Related

Handling simplexml to array elegantly

I have an xml structure:
<node1><node2><child_1/><child_2/><child_3/></node2></node1>
And i would want to get an array like this:
['child_1', 'child_2', 'child_3']
But to make my method for creating this handle errors elegantly and return an empty array when nothing found i am having to do this:
public function testXmlParse()
{
$config = new SimpleXMLElement("<node1><node2><child_1/><child_2/><child_3/></node2></node1>");
$result = $config->xpath('/node1/node2');
if (! count($result)) {
return [];
}
$result = $result[0]->children();
}
But i have even more code to write to check for arrays and valid etc.
Is there an elegantly way to get the correct result and return 0 on nothing finding?
The code you have written won't return an array anyway - the result of ->children() is an iterable SimpleXMLElement object. However, you can take advantage of the fact that a zero-element object is still iterable with foreach, and will simply go round zero times.
Since you are always looking for the first match, your example can also use SimpleXML access instead of XPath, to avoid the extra logic there.
$config = new SimpleXMLElement("<node1><node2><child_1/><child_2/><child_3/></node2></node1>");
// Start with an empty array; if no children are found, it will stay empty
$results = [];
// Note: $config represents the <node1> element, not the document
foreach ( $config->node2->children() as $name => $element ) {
$results[] = $name;
}
If <node2> is not always present, you may need to add an extra if ( isset($config->node2) ) around the loop, to avoid PHP throwing you warnings.

Convert foreach to for in PHP

foreach ( $this->parent->get_sections(null, $this->parent->author) as $section)
{
//...
}
I'm trying to do is force the loop to output each $section in the order I want. Each $section's name can be retrieved by $section->name. Let's say that I want to output $section "Section 2" first and then "Section 1" (and not in the order of the foreach). How can I force it do that? I presume the proper way would be a for loop with an if checking section names each time.
The proper way would be sorting the results when you call parent->get_sections(). How you would do this is entirely up to the implementation of that class and method. Changing this foreach to for for the sake of sorting seems like a code smell to me.
For the sake of answering the question as technical as possible.
$sections = $this->parent->get_sections(null, $this->parent->author);
$num_sections = count($sections);
for ($i = 0; $i < $num_sections; $i++) {
// what you do here is up to you $sections[$i]
}
Especially if you are not aware of the specific number of sections, you could use usort() to do a dynamic custom sort on the get_sections()-returned array or object and then utilize the existing code. (This is a little more elegant, imo, than doing the same in a for/foreach loop).
Not knowing the structure of your code, I would do something like.
// Get Org Sections
$sections = $this->parent->get_sections(null, $this->parent->author);
// Loop thru sections to get an array of names
foreach ( $sections as $key=>$section)
{
$sorted_sections[$section->name] = $key;
}
// Sort Array
//ksort — Sort an array by key
//krsort — Sort an array by key in reverse order
krsort($sorted_sections);
foreach ( $sorted_sections as $section)
{
// Orig Code
}
$section = $this->parent->get_sections(null, $this->parent->author);
echo $section[2]->name;
echo $section[1]->name;//just output the indexes the way you want
if you want it sorted, in say descending order, you can sort it that way and then use a for loop to display.

Accounting for missing array keys, within PHP foreach loop

I'm parsing a document for several different values, with PHP and Xpath. I'm throwing the results/matches of my Xpath queries into an array. So for example, I build my $prices array like this:
$prices = array();
$result = $xpath->query("//div[#class='the-price']");
foreach ($result as $object) {
$prices[] = $object->nodeValue; }
Once I have my array built, I loop through and throw the values into some HTML like this:
$i = 0;
foreach ($links as $link) {
echo <<<EOF
<div class="the-product">
<div class="the-name"><a title="{$names[$i]}" href="{$link}" target="blank">{$names[$i]}</a></div>
<br />
<div class="the-image"><a title="{$names[$i]}" href="{$link}" target="blank"><img src="{$images[$i]}" /></a></div>
<br />
<div class="the-current-price">Price is: <br> {$prices[$i]}</div>
</div>
EOF;
$i++; }
The problem is, some items in the original document that I'm parsing don't have a price, as in, they don't even contain <div class='the-price'>, so my Xpath isn't finding a value, and isn't inserting a value into the $prices array. I end up returning 20 products, and an array which contains only 17 keys/values, leading to Notice: Undefined offset errors all over the place.
So my question is, how can I account for items that are missing key values and throwing off my arrays? Can I insert dummy values into the array for these items? I've tried as many different solutions as I can think of. Mainly, IF statements within my foreach loops, but nothing seems to work.
Thank you
I suggest you look for an element inside your html which is always present in your "price"-loop. After you find this object you start looking for the "price" element, if there is none, you insert an empty string, etc. into your array.
Instead of directly looking for the the-price elements, look for the containing the-product. Loop on those, then do a subquery using those nodes as the starting context. That way you get all of the the-product nodes, plus the prices for those that have them.
e.g.
$products = array();
$products = $xpath->query("//div[#class='the-product']");
$found = 0 ;
foreach ($products as $product) {
$products[$found] = array();
$price = $xpath->query("//div[#class='the-price']", $product);
if ($price->length > 0) {
$products[$found] = $price->item(0)->nodeValue;
}
$found++;
}
If you don't want to show the products that don't have a price attached to them you could check if $prices[$i] is set first.
foreach($links AS $link){
if(isset($prices[$i])){
// echo content
}
}
Or if you wanted to fill it will dummy values you could say
$prices = array_merge($prices,
array_fill(count($prices), count($links)-count($prices),0));
And that would insert 0 as a dummy value for any remaining values. array_fill starts off by taking the first index of the array (so we start one after the amount of keys in $prices), then how many we need to fill, so we subtract how many are in $prices from how many are in $links, then we fill it with the dummy value 0.
Alternatively you could use the same logic in the first example and just apply that by saying:
echo isset($prices[$i]) ? $prices[$i] : '0';
Hard to understand the relation between $links and $prices with the code shown. Since you are building the $prices array without any relation to the $links array, I don't see how you would do this.
Is $links also built via xpath? If so, is 'the-price' div always nested within the DOM element used to populate $links?
If it is you could nest your xpath query to find the price within the query used to find the links and use a counter to match the two.
i.e.
$links_result = $xpath->query('path-to-link')
$i = 0
foreach ($links_result as $link_object) {
$links[$i] = $link_object->nodeValue;
// pass $link_object as context reference to xpath query looking for price
$price_result = $xpath->query('path-to-price-within-link-node', $link_object);
if (false !== $price_result) {
$prices[$i] = $price_result->nodeValue;
} else {
$prices[$i] = 0; // or whatever value you want to show to indicate that no price was available.
}
$i++;
}
Obviously, there could be additional handling in there to verify that only one price value exists per link node and so forth, but that is basic idea.

converting xml into array, unable to handle node inside a node

I want to convert a xml script into a associative array in PHP. The XML script is :
<ages>
<Peter>
<Peterchild>4</Peterchild>
<Peterchild>6</Peterchild>
</Peter>
<Quagmire>30</Quagmire>
<Joe>34</Joe>
</ages>
and the code i wrote for converting it to an array is
${$xml->getName()} = array();
foreach($xml->children() as $child){
$ages[$child->getName()] = (string)$child;
}
which gives the output as
Array
(
[Peter] =>
[Quagmire] => 30
[Joe] => 34
)
The problem is that I am unable to figure out a condition to recursively traverse through the children of children (children of Peter in this example). How to I change this code to consider those nodes as well?
You'll need to create a function that parses one level, then have the function call itself when it finds a node inside the current level. It's pretty complex and can get messy. Out of curiosity, why would you do this when you can access all the information you need in object format using a SimpleXML object?
${$xml->getName()} = array();
foreach($xml->children() as $child)
{
if (count($child->children() > 0)
{
foreach ($child->children as $childnode)
{
$ages[$childnode->getName()] = (string)$childnode;
}
}
else {
$ages[$child->getName()] = (string)$child;
}
}
This will only work if it is 1 level deep, if it is going to be more levels you should write a function, that will cycle through it level by level.

php and XML range into array

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');

Categories