Handling simplexml to array elegantly - php

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.

Related

Order alphabetically SimpleXML array by attribute

I need a bit of help ordering a SimpleXML array in an alphabetical order using one of it's attribute ($url). I have the following code that generate a page:
<?php
$xml = simplexml_load_file($xml_url);
$ids = array();
foreach($xml->list as $id) {
$ids[] = $id;
}
//It works but randomize instead of sorting alphabetically
usort($ids, function ($a, $b) {
return strnatcmp($a['url'], $b['url']);
});
foreach ($ids as $id) {
PopulatePage($id);
}
function PopulatePage($id) {
$url = $id->url;
$img = $id->img;
//OTHER CODES TO GENERATE THE PAGE
}?>
QUESTION RESOLVED!
There is no conversion needed, you already have an array which you can sort, you just need to understand how usort callbacks work. Each of the items in your $ids array is a SimpleXMLElement object, and each time your callback is run, it will be given two of those objects to compare. Those objects will be exactly the same as in the existing PopulatePage function, so accessing the URL needs to happen exactly as it does there ($url = $id->url;) not using a different notation ($id['url']).
To make it more explicit, let's write a named function with lots of clear variable names:
function CompareTwoIds(SimpleXMLElement $left_id, SimpleXMLElement $right_id): int {
$left_url = $left_id->url;
$right_url = $right_id->url;
return strncatcmp($left_url, $right_url);
}
Now you can test calling that function manually, and use it as the callback when you're happy:
usort($ids, 'CompareTwoIds');
Once you're comfortable with the principles, you may decide to skip the extra verbosity and just write this, which is completely equivalent:
usort($ids, fn($a,$b) => strncatcmp($a->url, $b->url));

Complex Loop through a complex SimpleXMLElement

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;
}

Checking the first element of array, regardless of array indexes

I have a need to check if the elements in an array are objects or something else. So far I did it like this:
if((is_object($myArray[0]))) { ... }
However, on occasion situations dictate that the input array does not have indexes that start with zero (or aren't even numeric), therefore asking for $myArray[0] will generate a Notice, but will also return the wrong result in my condition if the first array element actually is an object (but under another index).
The only way I can think of doing here is a foreach loop where I would break out of it right on the first go.
foreach($myArray as $element) {
$areObjects = (is_object($element));
break;
}
if(($areObjects)) { ... }
But I am wondering if there is a faster code than this, because a foreach loop seems unnecessary here.
you can use reset() function to get first index data from array
if(is_object(reset($myArray))){
//do here
}
You could get an array of keys and get the first one:
$keys = array_keys($myArray);
if((is_object($myArray[$keys[0]]))) { ... }
try this
reset($myArray);
$firstElement = current($myArray);
current gets the element in the current index, therefore you should reset the pointer of the array to the first element using reset
http://php.net/manual/en/function.current.php
http://php.net/manual/en/function.reset.php

delete variable by reference

given the following code:
$tree = array();
$node =& $tree[];
// imagine tons of code that populates $tree here
how can i entirely delete the ZVAL $node points to by reference? Is that even possible?
Using unset(), only the reference is destroyed and not the node in $tree itself:
unset($node);
print_r($tree);
// outputs:
Array
(
[0] =>
)
I know this is the expected behaviour of unset($reference) and I also know how the ZVAL refcounter works.
But i really need to delete that node after processing in a specific corner case.
Can i somehow find the correct array index and unset the array element directly like unset($tree[$node_index])?
Disclaimer: The above example is minified and isolated. Actually i'm modifying a complex parser for a really ugly nested table data structure that is presented as a stream. The code heavily uses pointers as backreferences and i'd like to avoid refactoring the whole code.
If you grab a reference to an array element and unset the reference the array will not be affected at all -- that's just how unset works, and this behavior is not negotiable.
What you need to do is remember the key of the element in question and unset directly on the array afterwards:
$tree = array();
$tree[] = 'whatever';
end($tree);
$key = key($tree);
// ...later on...
unset($tree[$key]);
Of course this is extremely ugly and it requires you to keep both $tree (or a reference to it) and $key around. You can mitigate this somewhat by packaging the unset operation into an anonymous function -- if there's a good chance you are going to pull the trigger later, the convenience could offset the additional resource consumption:
$tree = array();
$tree[] = 'whatever';
end($tree);
$key = key($tree);
$killThisItem = function() use(&$tree, $key) { unset($tree[$key]); } ;
// ...later on...
$killThisItem();

multi-dimensional stdClass Object

I've got a rather large multidimensional stdClass Object being outputted from a json feed with PHP.
It goes about 8 or 9 steps deep and the data that I need is around 7 steps in.
I'm wondering if I can easily grab one of the entires instead of doing this:
echo $data->one->two->anotherone->gettinglong->omg->hereweare;
I'm saying this because the data structure may change over time.
Is this possible?
You could try to parse the object into an array and search the array for the wanted values, it just keeps looping through each level of the object.
function parseObjectArrayToArray($objectPassed, &$in_arr = array()) {
foreach($objectPassed as $element) {
if(is_object($element) || is_array($element)) {
parseObjectArrayToArray($element,$in_arr);
} else {
// XML is being passed, need to strip it
$element = strip_tags($element);
// Trim whitespace
$element = trim($element);
// Push to array
if($element != '' && !in_array($element,$in_arr)) {
$in_arr[] = $element;
}
}
}
return $in_arr;
}
How to call
$parsed_obj_arr = parseObjectArrayToArray($objectPassed);
Not without searching through whats probably inefficient.
Json is a structured data object with the purpose of eliminating something like this.
If the datastructure can change, but doesn't very often, your best bet is to write a wrapper object so you will only have to change a path at a single point on change:
class MyDataWrapp {
public $json;
function __construct($jsonstring){
$this->json = json_decode($jsonstring);
}
function getHereWeAre(){
return $this->json->one->two->anotherone->gettinglong->omg->hereweare;
}
}
If the datastructure changes dramatically and constantly, I'd json_decode as an array of arrays, and probably use RecursiveFilterIterator.

Categories