I have a list of objects, each object could belong as a child of another object and each one has an element ('parent_id') that specifies which object it's a child of.
An example of what the expected tree should look like after children are properly imbedded:
Current Array
element 1
element 2
element 3
element 4
element 5
Organized into a hierarchy
-----------------------
element 2
-children:
element 1
-children
element 5
element 3
-children:
element 4
There should be two top elements, the first element would contain the element1 as a child, which would itself contain element 5 as a child. The third element would contain element 4 as a child.
The final result should produce a proper tree structure from it, however I'm running into an issue that when I generate the tree some of the elements are null.
From what I understand it is because of the weird scoping rules PHP has and that the last reference in the loop remains bound even after the end of the loop. So I included the unset($var) statements after each loop, however I'm still getting null values pushed into the tree.
Here is the fully contained example of the code:
$response = array(
array( "kind"=> "t1", "data" => array( "id" => 25, "parent_id" => 30 )),
array("kind"=> "t1", "data" => array( "id" => 30,"parent_id" => 0)),
array("kind"=> "t1", "data" => array("id" => 32, "parent_id" => 0 )),
array("kind"=> "t1", "data" => array("id" => 33,"parent_id" => 32)),
array("kind"=> "t1", "data" => array("id" => 35,"parent_id" => 25))
);
$json_str = json_encode($response);
$tree = array();
foreach($response as &$firstObj)
{
$parentFound = null;
foreach($response as &$secondObject)
{
if($firstObj['data']['parent_id'] == $secondObject['data']['id'])
{
$parentFound = &$secondObject;
break;
}
}
unset($secondObject);
if($parentFound)
{
$parentFound['data']['children'] = array(&$firstObj);
}
else{
$tree[] = $firstObj;
}
}
unset($firstObj);
print_r($tree);
The expected tree should contain only the topmost elements that are not children of other elements, the children should be embedded through references into the appropriate spaces of the top tree elements.
While I would probably opt for a recursive approach on a professional project, I've managed to produce a non-recursive approach using references like your posted code but with a single loop.
Code: (Demo)
$result = [];
foreach ($array as $obj) {
$id = $obj['data']['id'];
$parentId = $obj['data']['parent_id'];
if (isset($ref[$id])) { // child array populated before parent encountered
$obj['children'] = $ref[$id]['children']; // don't lose previously stored data
}
$ref[$id] = $obj;
if (!$parentId) {
$result[] = &$ref[$id]; // push top-level reference into tree
} else {
$ref[$parentId]['children'][] = &$ref[$id]; // push into parent-specific collection of references
}
}
var_export($result);
For a stacking process (not a recursive one) to build a hierarchical structure, your sample data is a little unexpected in that a child id integer is less than a parent id. I mean, in nature, parents are born first and then children are born. With your sample data, the input could not be pre-sorted by id value before looping -- this would have ensured that all parents where declared before children were encountered.
As a consequence, my snippet needs to push previously encountered children data into a newly encountered parent so that the declaration of the parent as a reference does not erase the cached children data.
I extended your sample data by one extra row while testing. If you find any breakages with my script, please supply new test data that exposes the issue.
I found the solution with the help of GPT/Bing:
So while I was unsetting the other variables, I wasn't unsetting the $parentFound, which has to be done as well.
Another thing was that I was not saving the item by reference when saving the item to the tree, which also has to be done in order for the whole reference tree to work.
The final code is:
$json_str = json_encode($response);
$tree = array();
foreach($response as &$firstObj)
{
$parentFound = null;
foreach($response as &$secondObject)
{
if($firstObj['data']['parent_id'] == $secondObject['data']['id'])
{
$parentFound = &$secondObject;
break;
}
}
if($parentFound)
{
$parentFound['data']['children'] = array(&$firstObj);
}
else{
$tree[] = &$firstObj; //has to be passed by reference
}
unset($secondObject);
unset($parentFound); //have to also include the $parentFound in the unset
}
unset($firstObj);
print_r($tree);
Related
I am looking at trying to build pagination method for an array. I have an array something like below. Before you suggest making the pagination work for sql query, I have already done so and it did work for a flat array but a requirement is having this multidimensional tree array.
array = (
item_id = 5,
parent_id = 0,
children = array(
array(
item_id = 20,
parent_id = 5,
children = array(
array(
item_id = 24,
parent_id = 20
),
array(
item_id = 24,
parent_id = 20
)
)
)
)
);
What methods that I can find don't seem to work with such an array since array_slice will only work on the first level of the array and doesn't take into consideration the children levels.
/*
$root =
[
'id' => 1,
'children' => [...]
]
$queue[] = $root
$visiteds[] = $root
while ($queue){
$current = array_shift($queue);
// do whatever with the current ex: echo $current."<br>"
if $current has children {
foreach child {
if ($child NOT in $visiteds) { // $child not visited before
$visiteds[] = $child // mark as visited
$queue[] = $child // add to queue
// do whatever with the child ex: echo $child ."<br>"
}
}
}
}
*/
Note: if you want to visit exactly same children as required (for example 2 exactly same child must be visited twice then, remove $visiteds related parts. In this case be careful that your graph structure must not have cycling.)
You may consider to read about Graph Theory, Breadth First Search algorithm, Depth First Search algorithm.
I have the following array:
$array = array(
array("2018","2019"),
"Jan",
array("France","Germany")
);
I need a matrix that crosses all the elements of the array; e.g:
array(
array("2018","Jan","France"),
array("2018","Jan","Germany"),
array("2019","Jan","France"),
array("2019","Jan","Germany")
);
meaning, 2 x 2 x 1 arrays
but this can be that I have more elements that are or are not arrays then:
$array = array(
array("2018","2019"),
"Jan",
array("France","Germany"),
array("prod1","prod2","prod3"),
'Act'
);
In this case I would get 2 x 2 x 1 x 4 x 1 arrays in the end.
Any idea on how to achieve this?
Is that what you are looking for ?
$dst = array(array());
foreach ($array as $idx => $val) {
foreach ($dst as $tmp_idx => $tmp_array) {
if (is_array($val)) {
foreach ($val as $sub_idx => $sub_val) {
$dst[] = array_merge($dst[$tmp_idx], array(count($dst[$tmp_idx]) => $sub_val));
}
} else {
$dst[] = array_merge($dst[$tmp_idx], array(count($dst[$tmp_idx]) => $val));
}
unset($dst[$tmp_idx]);
}
}
I declare the array with an empty array in it.
A first foreach iterates through the main array to access all the categories, whatever their number.
A second foreach iterates through the result array.
A third foreach - or not, depending if the category contains several or a single entry - iterates through each entry of the current category and merges an array containing only that current category in the result currently iterated on (from $dst). The merge is pushed into the result array. The result stored in $dst are assumed to be temporary and will be copied and completed with each new value, making them progressively contain one entry from each category.
At the end of each iteration on $dst, the current temporary result is removed from the array since it has no purpose : if the foreach iterated on it, that means that it was incomplete.
At the end, the only entries in $dst that remain are these who weren't iterated on, meaning that every entry from each category were considered when they were created, meaning that they are complete.
I'm available for any clarification may the need arise.
I have an XML which includes many thousands of nodes with information about them in attributes.
Each node is like :
<Class Option1="fiahfs;if" Option2="fiowfr0r0" ClassID="1">
<Class Option1="ro;ewaj;frwajro" Option2="afj;wh;fha" Option3="34014upkla" ClassID="2">
....
I need to parse that info into PHP arrays with array names being attribute names and the number of element in array equal to ClassID Attribute.
The problem is that some nodes have attributes that other nodes I dont have. I previously used ->attributes() for one selected element as
$a => $b,
$$a=array();
then
${$a}[(integer)$products['ClassID']]=$b
Where $products is simplexml element got from parsing XML with xpath. That basically gave me what i needed - i had a few arrays and i could address my requests like $Option1[1] , $Option2[1], Option1[2]...etc.
But the problem is that if I create that structure using only attribute list of one selected element -there`ll be elements that that one element do not have,but other have, and after a create arrays and then parse XML -there'll not be some arrays. Like if i create arrays from [0] of that example, that will give me $Option1, $Option2, but no $Option3.
When I rebuilt my code to :
foreach ($XML->xpath('/idspace/Class') as $products){
foreach($products->attributes() as $a => $b){
if(!isset($$a) $$a=array();
${$a}[(integer)$products['ClassID']]=$b;
}}
And after that I tried to foreach($ClassID) - I`ve got only one element in that array.
How can I parse every XML attribute to array while using attribute "ClassID" as element number in array?
I'm quite confused with your arrays. For me it sounds strange to have an extra array for each attribute. Why not make one array that contains each element and have the attributes as children. So that in the end you get an array like this:
$products = array(
[1] => array(
[Option1] => "fiahfs;if",
[Option2] => "fiowfr0r0",
),
[2] => array(
[Option1] => "ro;ewaj;frwajro",
[Option2] => "afj;wh;fha",
[Option3] => "34014upkla",
),
In this way you can easily access all the attributes for a given ChildId:
$products[$childId]['Option1'] - or better check if the Attribute-Name exists.
You can use simplexml->attributes()
$products = array(); // products will have the result!
foreach ($xml->products as $productXml) { // don't know your xml structure here,
// but you need to iterate over every product in the xml
$singleProduct = array();
$classId = 0;
foreach($productXml->attributes() as $a => $b) {
if ($a == 'ClassID') {
$classId = $b;
} else {
$singleProduct[$a] = $b;
}
}
$products[$classId] = $singleProduct;
}
in case of tl;dr: setting up array with references and then changing a copy of said array still preserves references and elements of all copies are changed at the same time. Need a workaround of replacing reference with the value that it points to.
I'm getting a complicated problem with using references in PHP Arrays :(
I have a flat array of unique elements with two properties: order and level.
Order represents element's order number from 1 to n in like table of contents way. Level represents which level "subchapter" the element is.
Example:
First brackets are element IDs which are random but unique:
[1][order:1][level:1]
----[7][order:2][level:2]
----[4][order:3][level:2]
---- ----[2][order:4][level:3]
[3][order:5][level:1]
[6][order:6][level:1]
----[5][order:7][level:2]
.
.
.
Remember, this is flat array of elements, above its just a visual representation.
Now I have tried to put them into array of form:
[1][children] => [
[7],
[4][children] => [
[2]
]
]
[3],
[6][children] => [
[5]
]
Which would represent a tree structure -ish..
I do this by first ordering them by order:
foreach($elements as $element){
$ordered_elements[$element['order']] = $element;
}
Then I shift each element under the correct parent:
foreach($ordered_elements as &$child){
if($child['level'] > 1){
$ordered_elements[$last_parent[$child['level']-1]]['children'][$child['content_id']] = &$child; // I think this is problematic line!!!
}
$last_parent[$child['level']] = $child['sort_order'];
}
Some of the elements stayed on root (first) level that shouldnt be there:
foreach($ordered_elements as &$child){
if($child['level'] == 1){
$ordered_elements[$child['content_id']] = $child;
}
unset($ordered_elements[$child['sort_order']]);
}
Now when this is done, the template array is ready. Now I start getting some data from query with element_id and user_id.
I want to set up a new table "users" that would have this previously made array for each user and I would be able to change its elements per user.
users[1]['elements'] = $ordered_elements;
users[2]['elements'] = $ordered_elements;
This function should return element by reference from user's own pool of elements, so we can change it directly into the users[x]['elements'][x]:
function &get_element_from_array(&$array, $searchValue){
$status = false;
foreach($array as $id => &$subtree) {
if ($id === $searchValue) {
return $subtree;
}
if (isset($subtree['children'])) {
$subsearch = &$this->get_element_from_array($subtree['children'], $searchValue);
if ($subsearch != false) {
return $subsearch;
}
}
}
return $status;
}
That means If i want to change element 5 from user 2 I need to call it like this:
$element = &get_element_from_array(users[2]['elements'], 5);
$element['visited'] = true;
This is where the problem occurs: I have just changed this element in user2 AND user1 array of elements.
I hope I didnt wrote this thing too long, was going for good explanation.
I'm creating a multi-dimensional array to match the hierarchy of a company. I ask the database for the downline (saved in a seperate table) and start looping it. Currently I'm only doing this for the highest level and the people below him. I know he has 6 people below him, thus the end result only shows one. When I var_dump $complete every time in the loop, I see different values in the children-array. (AKA he overwrites instead of adds up).
I've tried both array_push and this method, I hope I've been clear enough, because I probably don't (sorry.)
The code:
foreach ($downpositions as $downposition) {
// start position
if ($downposition['PositionsDownposition']['positionsdownpositions_rel_position'] == 1) {
// check downline only on first level
if ($downposition['PositionsDownposition']['positionsdownpositions_level'] == 1) {
foreach ($downpositions as $mother) {
if ($mother['PositionsDownposition']['positionsdownpositions_rel_downposition'] == 1) {
// format it
$node = array(
'id' => $downposition['PositionsDownposition']['positionsdownpositions_id'],
'name' => $downposition['PositionsDownposition']['positionsdownpositions_rel_position'],
'children' => array()
);
$complete = array(
'id' => $mother['PositionsDownposition']['positionsdownpositions_id'],
'name' => $mother['PositionsDownposition']['positionsdownpositions_rel_position'],
'children' => array()
);
// add up to array
$complete['children'][] = $node;
}
}
}
}
}
Found the problem, im intialising $complete['children'] in the loop, thus resetting the array to empty all the time.
Solution:
Re-do the code, initialise of $complete outside the loop.