1

So I have the following:

id           => the id of the folder
name         => the name of the folder
folder_id    => null if it's in the root, but will have the "id" of it's parent if it's inside another folder

I have all of these listed out in an array like so: (this will be $folder_array in my example)

Array (
    [1] => Array (
        [name] => folder1
        [id] => 1
        [folder_id] => null
    )
    [2] => Array (
        [name] => folder2
        [id] => 2
        [folder_id] => null
    )
    [3] => Array (
        [name] => folder3
        [id] => 3
        [folder_id] => 2
    )
    [4] => Array (
        [name] => folder4
        [id] => 4
        [folder_id] => 3
    )
}

I am trying to make an array that has a folder tree of sorts, so I'd like it to look like:

Array (
    [1] => Array (
        [name] => folder1
        [id] => 1
        [folder_id] => null
    )
    [2] => Array (
        [name] => folder2
        [id] => 2
        [folder_id] => null,
        [children] => Array (
            [3] => Array (
                [name] => folder3
                [id] => 3
                [folder_id] => 2,
                [children] => Array (
                    [4] => Array (
                        [name] => folder4
                        [id] => 4
                        [folder_id] => 3
                    )
                )
            )
        )
    )
}

So far I can get the first level of folders into it's right children array, but I'm having trouble getting multiple levels to go in.

Could anyone help me fix up this code so that I can make an effective folder tree. Also, if anyone has any more effective ways of organizing, I'd be open to that as well.

This is my code so far:

$folder_array = array(
    "1" => array("name"=> "folder1","id" => "1","folder_id" => null),
    "2" => array("name"=> "folder2","id" => "2","folder_id" => null),
    "3" => array("name"=> "folder3","id" => "3","folder_id" => "2"),
    "4" => array("name"=> "folder4","id" => "4","folder_id" => "3")
);

//a new array to store the folder tree in
$new_array = array();

//now search through each one that has no folder_id
foreach($folder_array as $folder)
{
    //if the folder id is empty, it means it is in the root(home) folder
    if(empty($folder['folder_id']))
    {
        $new_array[$folder['id']] = $folder_form_array[$folder['id']];

        //now go through folder_array again and see if it has any folders inside that one
        foreach($folder_array as $folder2)
        {
            //..and so on
        }
    }
}
bryan
  • 8,879
  • 18
  • 83
  • 166
  • maybe useful? [Category hierarchy from array(cat id => parent id)](https://stackoverflow.com/questions/27162873/category-hierarchy-from-arraycat-id-parent-id/). It is what i would base the answer on if required (my answer obviously). ;-/ The main difference is that you require to store 'data' in the nodes as well as the links but that will not affect the logic in any significant way imo. – Ryan Vincent Apr 01 '15 at 17:31
  • Thanks @RyanVincent, I'm reading it *trying* to understand. Definitely a good starting point. – bryan Apr 01 '15 at 18:42
  • In the: _insertNode_ function: your _[folder_id]_ maps to _'parentId'_ and _[id]_ maps to _childId_ parameters. It took me a while to understand it and debug it. ;-/ – Ryan Vincent Apr 01 '15 at 18:56
  • I know your onto something, I just can't wrap my head around it @RyanVincent :\ I do appreciate the help tho – bryan Apr 02 '15 at 03:02
  • I assume it loaded your 'tree' ok? Was fun to do. – Ryan Vincent Apr 03 '15 at 18:36

1 Answers1

1

This is a 'multi-way' tree with 'data' stored at the nodes.

Unless the source array is in order, such that 'parent' nodes come before 'child' nodes, then the tree will not build correctly if only one pass is used.

This version of the code will do multiple passes when trying to insert children.

To reduce the amount of array scanning, a separate list of nodes ($sourceKeys) is kept, and any inserted nodes are deleted from the list.

As usual: Working code at Eval.in - and - Full source code at Pastebin.com

Todo: test how expensive this is for large trees (maybe for an update?)

The function which tries to insert one node...

/**
 * Insert:  Find the 'parent' node
 *          if child not found then insert a 'node'
 *
 * @param array node passed by reference as the new node will be inserted
 * @param integer $parentId - root Id - may be null it indicate a 'root'
 * @param integer $childId
 * @param array   $folderInfo - the 'node data' to be stored

 * @return boolean  true if parentId found and child alive
 *                   false if parent not found anywhere in the tree
 */
function insertNode(&$node, $parentId, $childId, $folderInfo) {

    // is Root Node
    if (is_null($parentId)) {
        $node[$childId] = $folderInfo;
        $node[$childId]['children'] = array();
        return true;
    }

    if (isset($node[$parentId])) { // this node will be processed
        if (!isset($node[$parentId]['children'][$childId])) {
            $node[$parentId]['children'][$childId] = array_merge($folderInfo,
                                           array('children' => array())); // add child node
            return true;
        }
        return true; // end of processing
    }

    // check all the children of this node...
    foreach($node as $idx => &$child) { // need the reference
        if (count($child['children']) >= 1) { // check the children...
            if (insertNode($child['children'], $parentId, $childId, $folderInfo)) {
                return true;
            }
        }
    }
    return false; // parentId not in the tree
}

Start to process the data...

// extract the root nodes as we need those first...
$roots = array_filter($source, function($data) {
                                  return is_null($data['folder_id']);
                               });

// we need a list of child nodes that haven't been added yet
$sourceKeys = array_diff(array_keys($source), array_keys($roots));

Initialize output tree and process the root nodes...

$theTree = array();

// first we need to insert the roots... worth doing as a separate pass...
foreach($roots as  $idx => $folderInfo) {
    insertNode($theTree, $folderInfo['folder_id'], $folderInfo['id'], $folderInfo);
}

Now process the list of 'child nodes' that need to be added...

// now we do multiple passes down the source trying to insert nodes.
// We know we have finished when nothing is inserted during the pass.

// for efficiency, we will drive off the sourceKeys array after removing
// any inserted entries...

do {
    $insertCount = 0;

    foreach($sourceKeys as $position => $idx) {

        $folderInfo = $source[$idx];
        $inserted = insertNode($theTree, $folderInfo['folder_id'], $folderInfo['id'], $folderInfo);
        if ($inserted) {
            $insertCount++;
            unset($sourceKeys[$position]); // it is safe to do this in 'foreach'
        }
    }

} while ($insertCount > 0);

We are done... just some reporting to do...

// report nodes not inserted... this may be useful
foreach($sourceKeys as $idx) {
        var_dump('not inserted:', $source[$idx]);
}

// output the tree
echo '<pre>', 'Children are in the same order as the input array.', '<br />';
    print_r($theTree);
echo '</pre>';
exit;

Test Data - note: random order and one invalid node (666).

$source = Array (
   4 =>   Array("name" => "folder4",   "id" => 4,   "folder_id" => 3),
   11 =>  Array("name" => "folder11",  "id" => 11,  "folder_id" => 3),
   13 =>  Array("name" => "folder13",  "id" => 13,  "folder_id" => 12),
   31 =>  Array("name" => "folder31",  "id" => 31,  "folder_id" => 99),
   12 =>  Array("name" => "folder12",  "id" => 12,  "folder_id" => 98),
   42 =>  Array("name" => "folder42",  "id" => 42,  "folder_id" => 32),
   32 =>  Array("name" => "folder32",  "id" => 32,  "folder_id" => 99),
   666 => Array("name" => "folder666", "id" => 666, "folder_id" => 9999),
   99 =>   Array("name" => "folder99Root",   "id" => 99,   "folder_id" => null),
   3 =>   Array("name" => "folder3",   "id" => 3,   "folder_id" => 98),
   33 =>  Array("name" => "folder33",  "id" => 33,  "folder_id" => 99),
   98 =>   Array("name" => "folder98Root",   "id" => 98,   "folder_id" => null),
);
Ryan Vincent
  • 4,483
  • 7
  • 22
  • 31
  • Thank you! This is definitely close. But it doesn't seem to work if I have more than 2 child folders – bryan Apr 02 '15 at 15:40
  • @bryan - are you using the data in your question? Whatever: Go here: and check your data and code against what is there: It is the latest working code with extra test cases. [Codepad.org - questions/29396521/recursive-function-to-create-a-folder-tree-from-array](http://codepad.org/UWjuJw27). – Ryan Vincent Apr 02 '15 at 16:03
  • @bryan, The 'parents' of the new node that you add **must** be in the tree already. So the order of adding 'child' nodes ' is important. – Ryan Vincent Apr 02 '15 at 16:08
  • Ah I think that is my problem. That is definitely not how my folder list is ordered. I will have to see what I can do on that front but sounds a bit difficult to maintain if there is a LOT of folders and children. Thanks for getting back to me on the issue. – bryan Apr 02 '15 at 16:16
  • I do believe that the similarities in questions would cause confusion and it'd be better to just go to one place then to bounce around to different questions. If that's the only way I can get you to help through i'll gladly do it haha. But if not, [here is my new test data](http://pastebin.com/raw.php?i=7RDEeTDF). PS. You did help me immensely in solving the problem, so I will mark this as answered regardless; but it would be nice to get this to work with any type of ordered array. – bryan Apr 02 '15 at 16:30
  • @bryan, Ok, that sounds reasonable. I will explain the current limitations of the 'source' array. I will provide a new section that deals with 'random order' source arrays. In the first instance i will just do multiple passes over the source array. recording what happens. It is cheap to write but may be expensive for large arrays. Longer term, some type of 'sort' of the source array may be more beneficial. p.s. I want to do the 'random order' source anyway as it will make a very 'general' tree builder algorithm for me. :) – Ryan Vincent Apr 02 '15 at 16:49
  • That'd be awesome man, thanks so much for your help! Yea I'll definitely try and figure out a *sort* for long term but it should be fine for now. I'm glad you are able to kill 2 birds with 1 stone ;) – bryan Apr 02 '15 at 17:04