Recursive traversal of object - php

I'm having trouble writing a recursive function to traverse this hierarchical structure
object(stdClass)#290 (6) {
["category_id"]=>
int(1)
["parent_id"]=>
int(0)
["name"]=>
string(4) "Root"
["position"]=>
int(0)
["level"]=>
int(0)
["children"]=>
array(2) {
[0]=>
object(stdClass)#571 (7) {
["category_id"]=>
int(2)
["parent_id"]=>
int(1)
["name"]=>
string(18) "Root MySite.com"
["is_active"]=>
int(0)
["position"]=>
int(0)
["level"]=>
int(1)
["children"]=>
array(11) {
[0]=>
object(stdClass)#570 (7) {
["category_id"]=>
int(15)
["parent_id"]=>
int(2)
["name"]=>
string(9) "Widgets"
["is_active"]=>
int(1)
["position"]=>
int(68)
["level"]=>
int(2)
["children"]=>
array(19) {
[0]=>
object(stdClass)#566 (7) {
["category_id"]=>
int(24)
["parent_id"]=>
int(15)
["name"]=>
string(16) "Blue widgets"
["is_active"]=>
int(1)
["position"]=>
int(68)
["level"]=>
int(3)
["children"]=>
array(0) {
}
}
<snip....>
As you can see this nested set can go on forever.. .
What I want to return is something like this
$categories("Root" => array("Root MySite.com" => array("Widgets" => array("Blue Widgets",...))))
[EDIT] : pasting my starting point for my recursive function that will simply "flatten out" an arry or object. I would think i could modify this to make get the data structure I'm looking for but haven't been able to get it quite right.
function array_flatten($array, $return)
{
// `foreach` can also iterate through object properties like this
foreach($array as $key => $value)
{
if(is_object($value))
{
// cast objects as an array
$value = (array) $value;
}
if(is_array($value))
{
$return = array_flatten($value,$return);
}
else
{
if($value)
{
$return[] = $value;
}
}
}
return $return;
}
The question is I can't quite figure out to build the structure I'm looking for recursively, or maybe there is a more elegant php way to do this?

Try this
function run($o) {
$return = array();
foreach ($o->children as $child) {
$return[$child->name] = run($child);
}
return empty($return) ? null : $return;
}

I don't have time to write a working answer, but here's some pseudo code to do it (half PHP, half JS)
This would create a flattened version of the tree by removing the children property of each element in your list.
$flattened = array();
function addElement(&$flattened, $list) {
for ($element in $list) {
$children = $element->children;
delete $element->children;
$flattened[] = $element;
if ($children) {
addElements($flattened, $children)
}
}
}
addElements($flattened, $treeHierarchy);

deep_order object or assoc_array
namespace a_nsp
class object_utils{
static function deep_order($IN, $desc = NULL) { // object or assoc_array
$O = new \stdClass;
$I = (array) $IN;
$keys = array_keys($I);
$desc ? rsort($keys) : asort($keys);
foreach ($keys as $k) {
$v = $I[$k];
//$v = json_decode($I[$k],1) ?: $I[$k]; // force conversion of json_string
if (is_array($v) || is_object($v)) {
$O->$k = self::deep_order($v, $desc);
}
else {
$O->$k=$v;
}
}
return $O; // object
}
}
use like
$ordered_obj = \a_nsp\object_utils::deep_order($orig_obj)

Related

how to use explode for an array of objects

I have an array like that:
array(5) {
["code"]=>
int(1)
["messageError"]=>
string(27) "La typologie est incorrecte"
["model"]=>
string(3) "lot"
["grp_regles"]=>
array(1) {
[0]=>
array(1) {
[0]=>
array(3) {
["champ"]=>
string(21) "lot_surface_habitable"
["comparaison"]=>
string(7) "between"
["valeurAttendue"]=>
array(2) {
[0]=>
int(16)
[1]=>
int(40)
}
}
}
}
["prerequis"]=>
array(2) {
[0]=>
array(3) {
["champ"]=>
string(6) "typ_id"
["comparaison"]=>
string(1) "="
["valeurAttendue"]=>
int(1)
}
[1]=>
array(3) {
["champ"]=>
string(22) "tranche.fus.fup.fup_id"
["comparaison"]=>
string(1) "="
["valeurAttendue"]=>
int(1)
}
}
}
I want to do a foreach in "prerequis":
$modelRetrieve = $this->retrieveModel($model);
$modelFind = $modelRetrieve::find($id);
$arrayError=[];
$query = '';
$path = storage_path() . "/json/controle.json";
$json = file_get_contents($path);
foreach (json_decode($json,true) as $key => $value) {
$test = true;
var_dump($value);
if($value['model'] === $model ){
foreach ($value['prerequis'] as $key => $value2) {
if( $test && $modelFind[$value2['champ']] == (int)$value2["valeurAttendue"] )
{
$test = true;
}
}
}
}
I need in second foreach to use in $value2['champ'] where $value2['champ'] is "tranche.fus.fup_id. So I need to explode that to have ['tranche']['fus']['fup_id'].
How to use explode with that ?
thanks everyone :)
you can use laravel data_get helper:
data_get($value2, $value2['champ'])
To nest the $segments of the string starting with the innermost item of the result, we have to array_reverse() the $segments so we can loop over it and wrap each iteration's result with another array level in the next iteration until we looped through the whole $segments array.
$exploded = array_reduce(array_reverse(explode('.', $value2['champ'])), function($res, $segment) {
return [ $segment => $res ];
}, []);

How to make a sum of array values

I have an array and I iterate through its first 7 values with this function:
function clicksLast7Days($data)
{
$path = $data->data->data->clicks;
$num_loops = 0;
foreach ($path as $key => $item){
$num_loops++;
if($num_loops > 7) break;
if ($key >= 0) {
$array[] = $item->clicks;
}
}
return json_encode($array);
}
The array's structure (if it helps) is:
["data"]=>
object(stdClass)#212 (3) {
["status_code"]=>
int(200)
["data"]=>
object(stdClass)#211 (3) {
["days"]=>
int(30)
["total_clicks"]=>
int(6)
["clicks"]=>
array(30) {
[0]=>
object(stdClass)#215 (2) {
["clicks"]=>
int(0)
["day_start"]=>
int(1466395200)
}
[1]=>
object(stdClass)#216 (2) {
["clicks"]=>
int(0)
["day_start"]=>
int(1466308800)
}
[2]=>
object(stdClass)#217 (2) {
["clicks"]=>
int(0)
["day_start"]=>
int(1466222400)
}
[3]=>
object(stdClass)#218 (2) {
["clicks"]=>
int(0)
["day_start"]=>
int(1466136000)
}
[4]=>
object(stdClass)#219 (2) {
["clicks"]=>
int(0)
["day_start"]=>
int(1466049600)
}
[5]=>
object(stdClass)#220 (2) {
["clicks"]=>
int(0)
["day_start"]=>
int(1465963200)
}
[6]=>
object(stdClass)#221 (2) {
["clicks"]=>
int(0)
["day_start"]=>
int(1465876800)
}
[7]=>
object(stdClass)#222 (2) {
["clicks"]=>
int(0)
["day_start"]=>
int(1465790400)
}
The problem is that by using my function I get an array of elements:
[0,0,0,0,0,0,0]. But what I want is to obtain their sum (which in this case is 0), and not an array, just a number.
$clicksArray = $data->data->data->clicks;
$sum = array_sum(
array_map(
$clicksArray,
function($clickElement) {
return $clickElement->clicks;
}
)
);
What does this mean ?
array_sum will sum all values of a certain array. More information here
array_map will return all elements of an array, after applying a function (so, basically here, we are returning only clicks attribute

How can I make my function faster (the function is looping through a multidimensional array)?

My function is extremely slowly! Does anyone know what I did wrong or if it is possible to make it faster?
function explodeTree($array, $delimiter = "_", $baseval = false) {
if(!is_array($array)) return false;
$splitRE = "/" . preg_quote($delimiter, "/") . "/";
$returnArr = array();
foreach ($array as $key => $val) {
$parts = preg_split($splitRE, $val['path'], -1, PREG_SPLIT_NO_EMPTY);
$leafPart = array_pop($parts);
$parentArr = &$returnArr;
foreach ($parts as $part) {
if (!isset($parentArr[$part])) {
$parentArr[$part] = array();
} elseif (!is_array($parentArr[$part])) {
if ($baseval) {
$parentArr[$part] = array("__base_val" => $parentArr[$part]);
} else {
$parentArr[$part] = array();
}
}
$parentArr = &$parentArr[$part];
}
if (empty($parentArr[$leafPart])) {
$parentArr[$leafPart] = $val;
} elseif ($baseval && is_array($parentArr[$leafPart])) {
$parentArr[$leafPart]["__base_val"] = $val;
}
}
return $returnArr;
}
Array before:
array(4) {
[0]=>
array(3) {
["path"]=>
string(30) "Volumes/folder1/horse/fred"
["age"]=>
string(2) "12"
["name"]=>
string(4) "fred"
}
[1]=>
array(3) {
["path"]=>
string(28) "Volumes/folder1/cat/john"
["age"]=>
string(2) "10"
["name"]=>
string(4) "john"
}
[2]=>
array(3) {
["path"]=>
string(27) "Volumes/folder2/cat/sam"
["age"]=>
string(2) "11"
["name"]=>
string(3) "sam"
}
[3]=>
array(3) {
["path"]=>
string(32) "Volumes/folder2/cat/cat/john"
["age"]=>
string(2) "16"
["name"]=>
string(4) "john"
}
}
Array after using function:
array(1) {
["Volumes"]=>
array(2) {
["folder1"]=>
array(2) {
["horse"]=>
array(1) {
["fred"]=>
array(3) {
["path"]=>
string(30) "Volumes/folder1/horse/fred"
["age"]=>
string(2) "12"
["name"]=>
string(4) "fred"
}
}
["cat"]=>
array(1) {
["john"]=>
array(3) {
["path"]=>
string(28) "Volumes/folder1/cat/john"
["age"]=>
string(2) "10"
["name"]=>
string(4) "john"
}
}
}
["folder2"]=>
array(1) {
["cat"]=>
array(2) {
["sam"]=>
array(3) {
["path"]=>
string(27) "Volumes/folder2/cat/sam"
["age"]=>
string(2) "11"
["name"]=>
string(3) "sam"
}
["cat"]=>
array(1) {
["john"]=>
array(3) {
["path"]=>
string(32) "Volumes/folder2/cat/cat/john"
["age"]=>
string(2) "16"
["name"]=>
string(4) "john"
}
}
}
}
}
}
It seems your code produces the same output, no matter if $baseval is TRUE or FALSE. The following code produces the same output, runs fast and gracefully ignores the value of $baseval too:
function explodeTree(array $array, $delimiter = "_", $baseval = false)
{
# Build the output here
$returnArr = array();
foreach ($array as $item) {
# Split the path using the delimiter, drop the empty segments
$pieces = array_filter(explode($delimiter, $item['path']));
# Turn the path into a nested array
# Each component of the path is the only key on its level
# Build it from the leaf up to the root
$a = array_reduce(
array_reverse($pieces), # Start from the leaf
function (array $carry, $piece) { # Create parent node...
return array($piece => $carry); # ... use the path piece as key
},
$item # Put the item itself as leaf
);
# Combine the new path (nested arrays) into the existing tree
# array_merge_recursive() takes care of all the levels
$returnArr = array_merge_recursive($returnArr, $a);
}
# That's all
return $returnArr;
}
Adding $baseval
I think the purpose of $baseval is to put the original properties of an item into a new entry under the key __base_val if a subsequent path adds children to a leaf node. For example, if the last entry has 'Volumes/folder2/cat/sam/john' as path then the output of the current code ends with:
["folder2"]=>
array(1) {
["cat"]=>
array(1) {
["sam"]=>
array(4) {
["path"]=>
string(23) "Volumes/folder2/cat/sam"
["age"]=>
string(2) "11"
["name"]=>
string(3) "sam"
["john"]=>
array(3) {
["path"]=>
string(28) "Volumes/folder2/cat/sam/john"
["age"]=>
string(2) "16"
["name"]=>
string(4) "john"
}
}
}
}
but the expected output should be (I think):
["folder2"]=>
array(1) {
["cat"]=>
array(1) {
["sam"]=>
array(2) {
["__base_val"]=>
array(3) {
["path"]=>
string(23) "Volumes/folder2/cat/sam"
["age"]=>
string(2) "11"
["name"]=>
string(3) "sam"
}
["john"]=>
array(3) {
["path"]=>
string(28) "Volumes/folder2/cat/sam/john"
["age"]=>
string(2) "16"
["name"]=>
string(4) "john"
}
}
}
}
The above function cannot be modified to produce this output. array_merge_recursive() doesn't change the structure of the arrays it is provided to merge; it just combines them.
A complete rethink and rewrite of the function is needed:
function explodeTree(array $array, $delimiter = "_", $baseval = false)
{
# Build the output here
$returnArr = array();
foreach ($array as $item) {
# Split the path using the delimiter, drop the empty segments
$pieces = array_filter(explode($delimiter, $item['path']));
# Keep a reference to the current node; start from the root of the tree we build
$pos = &$returnArr;
foreach ($pieces as $piece) {
if (! array_key_exists($piece, $pos)) {
# The path component doesn't exist in the tree; add it
$pos[$piece] = array();
} elseif ($baseval && array_key_exists('path', $pos[$piece])) {
# The component exists, it is a leaf node (has 'path' property) and $baseval is TRUE
# Save the existing node content
$val = $pos[$piece];
# Replace it with a new level; store the old leaf in '__base_val'
$pos[$piece] = array('__base_val' => $val);
}
# Advance to the next level
$pos = &$pos[$piece];
}
# If $baseval is TRUE, make sure we don't mix leaf nodes with inner nodes
if ($baseval && ! empty($pos)) {
# The node already has children; put the item in '__base_val'
$pos['__base_val'] = $item;
} else {
# The node was just added; store $item in it
$pos = array_merge($pos, $item);
}
unset($pos);
}
return $returnArr;
}
You might be running into huge amounts of memory reallocations. If you add a $begin = microtime(true); at the top of the outer foreach, and then printf('%0.6f', microtime(true) - $begin); at the end of the loop, do you see the times staying at a roughly stable level or does it get slower as the execution advances?
It looks like you're building a trie. In which case https://github.com/ikcede/PHP-Trie may be of use.

Get path to parent node of (parent/child) array

Given the following array:
array(..) {
[1111]=>
&array(3) {
["category_id"]=>
int(1111)
["parent_id"]=>
int(0)
["children"]=>
array(2) {
[2222]=>
&array(3) {
["category_id"]=>
int(2222)
["parent_id"]=>
int(1111)
["children"]=>
array(2) {
[5555]=>
&array(1) {
["category_id"]=>
int(5555)
["parent_id"]=>
int(2222)
}
}
[3333]=>
&array(2) {
["category_id"]=>
int(3333)
["parent_id"]=>
int(1111)
}
}
Assuming that the array depth is unknown, How can I determine the path to the parent node?
For example, for category_id = 5555 I should get the following result: 0, 1111,2222. For category_id = 3333 it would be 0,1111
One approach:
function findParent($node, $array, &$parents) {
if (array_key_exists($node, $array)) {
$parents[] = $array[$node]['parent_id'];
}
else {
foreach ($array as $item) {
if (array_key_exists('children', $item)) {
$parents[] = $item['parent_id'];
findParent($node, $item['children'], $parents);
}
}
}
}
findParent(5555, $A, $parents);

Recursive JSON transformation

Suppose I have a JSON string:
$json = '{"lemon":"test",
"orange":["toto", "tata", "tete"],
"zob":[{"id":"0"}, {"id":"1"}]}';
I'd like to cycle through that encoded object to modify every string in it, so I have a recursive function:
function doObject($__obj){
$__obj = cycleObject($__obj);
return $__obj;
}
function cycleObject($__obj){
$type = gettype($__obj);
foreach($__obj as $var => &$val){
switch(gettype($val)){
case 'object':
cycleObject($val);
break;
case 'array':
cycleObject($val);
break;
case 'string':
if($type == 'object'){
$__obj->$var = $val.'-ok';
}else{
if($type == 'array'){
$__obj[$var] = $val.'-ok';
}
}
break;
}
}
return $__obj;
}
And I call the function:
$obj = doObject(json_decode($json));
var_dump($obj);
Which gives :
object(stdClass)#1 (3) {
["lemon"]=> string(7) "test-ok"
["orange"]=> array(3) {
[0]=> string(4) "toto"
[1]=> string(4) "tata"
[2]=> string(4) "tete" }
["zob"]=> array(2) {
[0]=> object(stdClass)#2 (1) {
["id"]=> string(4) "0-ok" }
[1]=> object(stdClass)#3 (1) {
["id"]=> string(4) "1-ok" }
}
}
Now my problem is, for some reason, I am unable to modify directly inside an array composed by string, or should I say, the modified string inside an array (and not inside an object inside an array) because the array loses its reference. How do I fix that so in orange I instead obtain:
[0]=> string(7) "toto-ok"
[1]=> string(7) "tata-ok"
[2]=> string(7) "tete-ok"
Your array of strings isn't being scrutinized correctly by your function. Basically, in each array you need a second check to see if you are dealing with another array/object or a string, otherwise regular arrays of strings are being bypassed....oddly enough. The following should work for you:
$json = '{"lemon":"test",
"orange":["toto", "tata", "tete"],
"zob":[{"id":"0"}, {"id":"1"}]}';
function doObject($__obj){
$__obj = cycleObject($__obj);
return $__obj;
}
function cycleObject($__obj){
foreach($__obj as $key => &$val){
if(is_object($val)) {
cycleObject($val);
}
if(is_array($val)) {
foreach($val as &$v) {
if(is_object($v) || is_array($v)) {
cycleObject($v);
} else {
$v .= '-ok';
}
}
}
if(is_string($val)) {
$val .= '-ok';
}
}
return $__obj;
}
$obj = doObject(json_decode($json));
var_dump($obj);
This produced the results you were looking for in my local environment.
object(stdClass)#1 (3) {
["lemon"]=>
string(7) "test-ok"
["orange"]=>
array(3) {
[0]=>
string(7) "toto-ok"
[1]=>
string(7) "tata-ok"
[2]=>
string(7) "tete-ok"
}
["zob"]=>
array(2) {
[0]=>
object(stdClass)#2 (1) {
["id"]=>
string(4) "0-ok"
}
[1]=>
object(stdClass)#3 (1) {
["id"]=>
string(4) "1-ok"
}
}
}

Categories