11

I've got a categories table. Each category can have an optional parent (Defaults to 0 if no parent).

What I want to do is build a simple html list tree with the levels of the categories.

Example date:

Foods
-- Fruit
---- Apple
---- Banana
---- Orange
-- Veg
---- Cucumber
---- Lettuce
Drinks
-- Alcoholic
---- Beer
---- Vodka
Misc
-- Household Objects
---- Kitchen
------ Electrical
-------- Cooking
---------- Stove
---------- Toaster
---------- Microwave

Note that this needs to work for around 10 'levels'. I'd love it to be infinite but I really dont want to be going down the route of using a nested set model as it'll cause huge delays on this project.

The docs on this for laravel are terrible, with no real reference as to where to even start. I've been playing with it for days trying to work out what to do and seem to be getting nowhere without a huge messy block of for each loops within each other 10 times.

I've got my tree of data using the following in my model:

<?php
class Item extends Eloquent {
    public function parent()
    {
        return $this->hasOne('Item', 'id', 'parent_id');
    }

    public function children()
    {
        return $this->hasMany('Item', 'parent_id', 'id');
    }

    public function tree()
    {
        return static::with(implode('.', array_fill(0,10, 'children')))->where('parent_id', '=', '0')->get();
    }
}

This gets all the parent and children up to a level of 10. This works fine, but you cant really then do anything with the child data without manually having 10 foreach loops within each other.

What am I doing wrong here? Surely this shouldn't be this hard/poorly executed? All I want do do is get a simple html list with the items in a tree structure.

I've put together a quick SQLFiddle example of the dummy data used above: http://sqlfiddle.com/#!2/e6d18/1

StackOverflowNewbie
  • 39,403
  • 111
  • 277
  • 441
Sk446
  • 1,240
  • 3
  • 19
  • 38
  • Nested sets are actually simpler. Plus, there are some existing packages already, like [Cartalyst's Nested Sets](http://docs.cartalyst.com/nested-sets-2) or [Baum](https://github.com/etrepat/baum). – Jason Lewis May 15 '13 at 01:23
  • Jason, what is the advantage of nested sets compared to the accepted answer? I have to decide between the two. And with Baum, there seems no way to retrieve that tree anyway, unless you use the accepted answer as well. – kJamesy Oct 25 '13 at 10:38

3 Answers3

39

This was much more fun than my usual morning crossword puzzle. :)

Here is an ItemsHelper class that will do what you are looking for, and better yet will recurse as far down as you want.

app/models/ItemsHelper.php:

<?php

  class ItemsHelper {

    private $items;

    public function __construct($items) {
      $this->items = $items;
    }

    public function htmlList() {
      return $this->htmlFromArray($this->itemArray());
    }

    private function itemArray() {
      $result = array();
      foreach($this->items as $item) {
        if ($item->parent_id == 0) {
          $result[$item->name] = $this->itemWithChildren($item);
        }
      }
      return $result;
    }

    private function childrenOf($item) {
      $result = array();
      foreach($this->items as $i) {
        if ($i->parent_id == $item->id) {
          $result[] = $i;
        }
      }
      return $result;
    }

    private function itemWithChildren($item) {
      $result = array();
      $children = $this->childrenOf($item);
      foreach ($children as $child) {
        $result[$child->name] = $this->itemWithChildren($child);
      }
      return $result;
    }

    private function htmlFromArray($array) {
      $html = '';
      foreach($array as $k=>$v) {
        $html .= "<ul>";
        $html .= "<li>".$k."</li>";
        if(count($v) > 0) {
          $html .= $this->htmlFromArray($v);
        }
        $html .= "</ul>";
      }
      return $html;
    }
  }

I just used a new installation of Laravel 4 and the basic hello.php view.

Here is my route in app/routes.php:

Route::get('/', function()
{
  $items = Item::all();
  $itemsHelper = new ItemsHelper($items);
  return View::make('hello',compact('items','itemsHelper'));
});

Although my view doesn't use the items variable, I'm passing it here because you probably will want to do something else with them too.

And finally, my app/views/hello.php just has one line:

<?= $itemsHelper->htmlList(); ?>

The output looks like this:

  • Foods
    • Fruit
      • Apple
      • Banana
      • Orange
    • Veg
      • Cucumber
      • Lettuce
  • Drinks
    • Alcoholic
      • Beer
      • Vodka
  • Misc
    • Household Objects
      • Kitchen
        • Electrical
          • Cooking
            • Stove
            • Toaster
            • Microwave

Note: your SQL Fiddle had 5 ("Orange") as the parent_id for Cucumber and Lettuce, I had to change it to 6 ("Veg").

Mark Smith
  • 621
  • 6
  • 8
  • 1
    @MarkSmith - great answer thank you! Just struggling to understand how you would add additional values to the array? Or even add named values? For example: `{ id: '2', name: 'Parent 2', children: [ { id: '5', name: 'Sub 2a' }, { id: '6', name: 'Sub 2b' } ] }` – DavidP Dec 22 '13 at 13:20
  • Infinitely helpful! THANKS! – jeanfrg Jul 17 '14 at 19:38
  • I'd also like to know how to add additional values... @MarkSmith – dcolumbus Sep 01 '14 at 22:16
  • This is a good solution, but there's only 1 thing, that is not good. The problem is that the view is generated in model, not the view itself, and if you following MVC pattern, you should not have any HTML anywhere except view files. There is another solution, with recursive calls of the blade sub-templates, but still not a good one, though it follow MVC pattern – ArmeniaH Oct 03 '14 at 09:32
  • Sweet! Is there a way to return the whole object that is being iterated? Instead of just the name? – Albin N Jan 15 '15 at 14:15
  • See my solution below which expands on the accepted answer but handles additional data as requested. – Amo Jan 19 '15 at 13:40
2

I've expanded on the accepted answer by Mark Smith to allow generated lists to reference addition data that is passed into the class.

The class works in pretty much the same way, but I've packaged it up so hopefully it can be used easily.

Just reference the helper class in your controller:

use App\Helpers\CategoryHierarchy;

You can then either instantiate the class manually, or using Laravel 5's method injection things get even better:

$products = $product->getAllProducts();
$hierarchy->setupItems($products);
return $hierarchy->render();

This can output the following:

<ul class='simple-list'>
   <li><input type="checkbox" name="hierarchy-checkboxes[]" value="1" >Home delivery</li>
      <ul>
        <li><input type="checkbox" name="hierarchy-checkboxes[]" value="2" >Italian</li>
          <ul>
            <li><input type="checkbox" name="hierarchy-checkboxes[]" value="3" >Pizza</li>
            <li><input type="checkbox" name="hierarchy-checkboxes[]" value="4" >Pasta</li>
          </ul>
        <li><input type="checkbox" name="hierarchy-checkboxes[]" value="5" >Burgers</li>
      </ul>
</ul>

A repo is available: https://github.com/amochohan/CategoryHierarchy which explains in some more detail.

Amo
  • 2,884
  • 5
  • 24
  • 46
1

I use this functions to make it work.

 //Returns Root elements
 public function scopeRoot($query) {
        $all = $query->whereParent(0)->get();
        $collection = $all->filter(function($single) {
            if ($single->ModelFilter('GET')) {
                return $single;
            }
        });
        return $collection;
    }
    //Recursive call
    public function traverse() {
        self::_traverse($this->Children, $array, $this);
        return $array;
    }

    //This function build a multidimensional array based on a collection of elements
    private static function _traverse($collection, &$array, $object) {
        $new_array = array();
        foreach ($collection as $element) {
            self::_traverse($element->Children, $new_array, $element);
        }
        $array[] = $object;
        if (count($new_array) > 0) {
            $array[] = $new_array;
        }
    }

First I get a collection of the root elements those are the ones that I pass to my views where I want to list the tree...

Then I do ...

 <ul class="bg-info cat-list">
        @foreach($categories as $category)
        <?php
        $array = $category->traverse();
        list_view($array);
        ?>
        @endforeach
    </ul>

Using this function...

//Prints a multidimensional array as a nested HTML list
function list_view($element, $ul = true) {
    foreach ($element as $value) {
        if (!is_array($value)) {
            echo "<li>";
            echo $value->name;
        } else {
            echo ($ul) ? "<ul>" : "<ol>";
            list_view($valuce, $ul);
            echo "</li>";
            echo ($ul) ? "</ul>" : "</ol>";
        }
    }
}

Output

Hope it helps