PHP tree structure for categories and sub categories without looping a query

69,050

Solution 1

This does the job:

$items = array(
        (object) array('id' => 42, 'parent_id' => 1),
        (object) array('id' => 43, 'parent_id' => 42),
        (object) array('id' => 1,  'parent_id' => 0),
);

$childs = array();

foreach($items as $item)
    $childs[$item->parent_id][] = $item;

foreach($items as $item) if (isset($childs[$item->id]))
    $item->childs = $childs[$item->id];

$tree = $childs[0];

print_r($tree);

This works by first indexing categories by parent_id. Then for each category, we just have to set category->childs to childs[category->id], and the tree is built !

So, now $tree is the categories tree. It contains an array of items with parent_id=0, which themselves contain an array of their childs, which themselves ...

Output of print_r($tree):

stdClass Object
(
    [id] => 1
    [parent_id] => 0
    [childs] => Array
        (
            [0] => stdClass Object
                (
                    [id] => 42
                    [parent_id] => 1
                    [childs] => Array
                        (
                            [0] => stdClass Object
                                (
                                    [id] => 43
                                    [parent_id] => 42
                                )

                        )

                )

        )

)

So here is the final function:

function buildTree($items) {

    $childs = array();

    foreach($items as $item)
        $childs[$item->parent_id][] = $item;

    foreach($items as $item) if (isset($childs[$item->id]))
        $item->childs = $childs[$item->id];

    return $childs[0];
}

$tree = buildTree($items);

Here is the same version, with arrays, which is a little tricky as we need to play with references (but works equally well):
$items = array(
        array('id' => 42, 'parent_id' => 1),
        array('id' => 43, 'parent_id' => 42),
        array('id' => 1,  'parent_id' => 0),
);

$childs = array();
foreach($items as &$item) $childs[$item['parent_id']][] = &$item;
unset($item);

foreach($items as &$item) if (isset($childs[$item['id']]))
        $item['childs'] = $childs[$item['id']];
unset($item);

$tree = $childs[0];

So the array version of the final function:

function buildTree($items) {
    $childs = array();

    foreach($items as &$item) $childs[(int)$item['parent_id']][] = &$item;

    foreach($items as &$item) if (isset($childs[$item['id']]))
            $item['childs'] = $childs[$item['id']];
   
    return $childs[0]; // Root only.
}

$tree = buildTree($items);

Solution 2

You can fetch all categories at once.

Suppose you have a flat result from the database, like this:

$categories = array(
    array('id' => 1,  'parent' => 0, 'name' => 'Category A'),
    array('id' => 2,  'parent' => 0, 'name' => 'Category B'),
    array('id' => 3,  'parent' => 0, 'name' => 'Category C'),
    array('id' => 4,  'parent' => 0, 'name' => 'Category D'),
    array('id' => 5,  'parent' => 0, 'name' => 'Category E'),
    array('id' => 6,  'parent' => 2, 'name' => 'Subcategory F'),
    array('id' => 7,  'parent' => 2, 'name' => 'Subcategory G'),
    array('id' => 8,  'parent' => 3, 'name' => 'Subcategory H'),
    array('id' => 9,  'parent' => 4, 'name' => 'Subcategory I'),
    array('id' => 10, 'parent' => 9, 'name' => 'Subcategory J'),
);

You can create a simple function that turns that flat list into a structure, preferably inside a function. I use pass-by-reference so that there are only one array per category and not multiple copies of the array for one category.

function categoriesToTree(&$categories) {

A map is used to lookup categories quickly. Here, I also created a dummy array for the "root" level.

    $map = array(
        0 => array('subcategories' => array())
    );

I added another field, subcategories, to each category array, and add it to the map.

    foreach ($categories as &$category) {
        $category['subcategories'] = array();
        $map[$category['id']] = &$category;
    }

Looping through each categories again, adding itself to its parent's subcategory list. The reference is important here, otherwise the categories already added will not be updated when there are more subcategories.

    foreach ($categories as &$category) {
        $map[$category['parent']]['subcategories'][] = &$category;
    }

Finally, return the subcategories of that dummy category which refer to all top level categories._

    return $map[0]['subcategories'];

}

Usage:

$tree = categoriesToTree($categories);

And here is the code in action on Codepad.

Solution 3

See the method :

function buildTree(array &$elements, $parentId = 0) {

    $branch = array();    
    foreach ($elements as $element) {
        if ($element['parent_id'] == $parentId) {
            $children = buildTree($elements, $element['id']);
            if ($children) {
                $element['children'] = $children;
            }
            $branch[$element['id']] = $element;
        }
    }
    return $branch;
}
Share:
69,050
Admin
Author by

Admin

Updated on May 11, 2021

Comments

  • Admin
    Admin about 3 years

    I'm trying to create a list of categories with any number of sub categories, where sub categories can also has their own sub categories.

    I have selected all categories from the Mysql db, the cats are in a standard associate array list, each category has an id, name, parentid where the parentid is 0 if it's top level.

    I basically want to be able to take the single level array of cats and turn it into a multidimensional array structure where each category can have an element which will contain an array of subcats.

    Now, I can easily achieve this by looping a query for each category but this is far from ideal, I'm trying to do it without any extra hits on the db.

    I understand I need a recursive function for this. Can anyone point me in the right direction for this tree style structure?

    Cheers

  • Andreyco
    Andreyco over 11 years
    Nice solution! This may consume more server memory but should be more efficied when processing large sets of categories.
  • juwens
    juwens over 11 years
    A few curly braces would not have hurt ;)
  • DreaminMedia Queretaro
    DreaminMedia Queretaro about 11 years
    Sorry if it is a fool question, to set the array from the database , i just make the query and then assign to a variable, and then asign that variable to array(). And also , how can you print the tree as a select box stackoverflow.com/questions/16470154/…
  • vladkras
    vladkras over 7 years
    exactly same as this answer except useless reference in param &$elements and id in branch
  • Paweł Baca
    Paweł Baca over 5 years
    Good job! Thanks