1

I'm working on a project using jstree and attempting to save it to my database.

I'm having difficulties understanding how to do this because the user is able to create as many nodes as they want, to an unlimited depth.

For example consider the following tree:

enter image description here

When this is posted to PHP the array is as follows. Note how the children elements appear:

    $tree[0]['id'] = 'loc1';
    $tree[0]['text'] = 'Sector';

    $tree[1]['id'] = 'loc2';
    $tree[1]['text'] = 'Location';
    $tree[1]['children'][0]['id'] = 'italy-1';
    $tree[1]['children'][0]['text'] = 'Italy';
    $tree[1]['children'][1]['id'] = 'poland-1';
    $tree[1]['children'][1]['text'] = 'Poland';

    $tree[2]['id'] = 'j1_1';
    $tree[2]['text'] = 'abc';
    $tree[2]['children'][0]['id'] = 'j1_2';
    $tree[2]['children'][0]['text'] = 'def';
    $tree[2]['children'][0]['children'][0]['id'] = 'france-1';
    $tree[2]['children'][0]['children'][0]['text'] = 'France';
    $tree[2]['children'][0]['children'][1]['id'] = 'germany-1';
    $tree[2]['children'][0]['children'][1]['text'] = 'Germany';

    $tree[3]['id'] = 'j1_5';
    $tree[3]['text'] = 'zzz';

My problem is that I don't understand is how to loop through the 'children' elements of the array - because the depth varies between each parent node. If it was only 1 level deep I could use 1 foreach statement and then check on the presence of [n]['children'] and then loop over the items inside it.

To further complicate matters I am saving the data using CakePHP 2.x Tree behaviour. This requires me to specify the parent ID when saving child elements, which means I need to loop through the array in order. For example if I was saving 'France' (under 'abc' > 'def') it would have to be done like this:

$data['Tree']['parent_id'] = 'j1_2'; // ID of 'def'
$data['Tree']['name'] = 'France';
$this->Tree->save($data);

Can anyone advise on how to loop through this data without the need to nest multiple foreach statements? I have read Is there a way to loop through a multidimensional array without knowing it's depth? but couldn't apply this or understand if/how it's relevant to what I'm trying to do.

Andy
  • 5,142
  • 11
  • 58
  • 131
  • There's a whole bunch of neat iterator classes you can leverage at http://php.net/manual/en/spl.iterators.php – Scuzzy Nov 29 '17 at 11:55
  • Thanks for the pointers but I need more guidance than that. I need a worked example of how to apply this to my actual case. I've read so much about arrays but can't see how to do what I need in this case. Thanks though. – Andy Nov 29 '17 at 11:59

2 Answers2

2

So, when dealing with tree structures you definitely need some recursion and iterators. Possible solution may look like:

/**
 * We need to extend a recursive iterator from SPL library.
 * You can reed more about it here 
 * http://php.net/manual/en/class.recursivearrayiterator.php
 */
class TreeIterator extends RecursiveArrayIterator
{
    /*
     * Originally this method returns true if current element is an array or an object
     * http://php.net/manual/en/recursivearrayiterator.haschildren.php
     * But in our case this behaviour is not suitable, so we have to redefine this method
     */
    public function hasChildren() : bool
    {
        // getting element that is used on current iteration
        $current = $this->current();

        // checking if it has any children
        return (bool) ($current['children'] ?? null);
    }

    /*
     * Originally this method returns new instance of the RecursiveArrayIterator
     * with elements that current element contains
     * http://php.net/manual/en/recursivearrayiterator.getchildren.php
     * And we have to redefine it too
     */
    public function getChildren() : self
    {
        // getting element that is used on current iteration
        $current = $this->current();

        // extracting array of child elements or assign empty array to gracefully avoid errors if it doesn't exist
        $children = $current['children'] ?? [];

        // adding to every child element id of the parent element
        array_walk($children, function (&$child) use ($current) {
            $child['parent_id'] = $current['id'];
        });

        // return new instance of our iterator
        return new self($children);
    }
}

// simply create an instance of the class with tree structure passed as an argument to constructor
$iterator = new TreeIterator($tree);

// apply the handler function for every element in an iterator
// http://php.net/manual/en/function.iterator-apply.php
// you also can use any another valid approach to iterate through it
iterator_apply($iterator, 'traverseStructure', [$iterator]);

function traverseStructure($iterator) {
    // iterate through iterator while it has any elements
    while ($iterator->valid()) {
        // get current element
        $current = $iterator->current();

        /** start: replace this block with your actual saving logic **/
        $output = sprintf('id:%10s, text:%10s', $current['id'], $current['text']);

        if (isset($current['parent_id'])) {
            $output .= sprintf(', parent_id:%10s', $current['parent_id']);
        }

        echo $output . PHP_EOL;
        /** end: replace this block with your actual saving logic **/

        // check if current element has children
        if ($iterator->hasChildren()) {
            // if it has - get children of the current element and pass it to the same function
            // we are using some recursion here
            traverseStructure($iterator->getChildren());
        }

        $iterator->next();
    }
}

Output for this script looks like:

id:      loc1, text:    Sector
id:      loc2, text:  Location
id:   italy-1, text:     Italy, parent_id:      loc2
id:  poland-1, text:    Poland, parent_id:      loc2
id:      j1_1, text:       abc
id:      j1_2, text:       def, parent_id:      j1_1
id:  france-1, text:    France, parent_id:      j1_2
id: germany-1, text:   Germany, parent_id:      j1_2
id:      j1_5, text:       zzz
  • That works and does exactly what I need. Would you be able to add some notes or comments about how the code works? I can follow the basics of what you're doing but don't fully understand how it works, and would like to know. Many thanks. – Andy Nov 29 '17 at 13:20
  • 1
    I've added some comments, hope those will help. – Oleksandr Zaitsev Nov 29 '17 at 13:50
0

You just need to use a recursive function.

<?php

function iterate_tree($tree, $level, $parent_id) {
    foreach($tree as $index => $node) {
        foreach($node as $index => $value) {
            if($index == "children") {
                iterate_tree($value, $level + 1, $node['id']);
            } else if($index == "id") {
                $data['id'] = $node['id'];
                $data['text'] = $node['text'];
                if($parent_id != '') {
                    $data['parent_id'] = $parent_id;
                }
                echo "Level: $level <br>Data: ";
                print_r($data);
                echo "<br><br>";
            }
        }
    }
}

iterate_tree($tree, 0, '');

?>

Gives the following output:

Level: 0 
Data: Array ( [id] => loc1 [text] => Sector ) 

Level: 0 
Data: Array ( [id] => loc2 [text] => Location ) 

Level: 1 
Data: Array ( [id] => italy-1 [text] => Italy [parent_id] => loc2 ) 

Level: 1 
Data: Array ( [id] => poland-1 [text] => Poland [parent_id] => loc2 ) 

Level: 0 
Data: Array ( [id] => j1_1 [text] => abc ) 

Level: 1 
Data: Array ( [id] => j1_2 [text] => def [parent_id] => j1_1 ) 

Level: 2 
Data: Array ( [id] => france-1 [text] => France [parent_id] => j1_2 ) 

Level: 2 
Data: Array ( [id] => germany-1 [text] => Germany [parent_id] => j1_2 ) 

Level: 0 
Data: Array ( [id] => j1_5 [text] => zzz ) 
Xpleria
  • 5,472
  • 5
  • 52
  • 66