Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel Menu self recursion

the model

 class Menu extends Eloquent {

        public static $table = 'menus';

        public function parent_menu()
        {
            return $this->belongs_to('Menu', 'parent_id');
        }

    }

how I get it in the controller:

$menus = Menu::with('parent_menu')->get();

how do I render it in the view:

foreach($menus as $m)
{
  echo $m->parent_menu->title;
}

looks like there is a problem when the relation is inside a table, i get an error

`trying to get property of non object`

is there a solution for this?

like image 827
Hello Avatar asked May 04 '13 22:05

Hello


3 Answers

I have implemented a way to get endless depth in menu's in Laravel 4. It is not exactly what you ask, but the technique should be easily adaptable.

For starters my menu is just an array (for now) that gets assigned to the master view and that looks something like this.

$menu = array(
   array(
     'name' => 'item1',
     'url' => '/'
   ),
    array(
     'name' => 'item2',
     'url' => '/',
     'items' => array(
        array(
          'name' => 'subitem1',
          'url' => '/'
        ),
        array(
          'name' => 'subitem2',
          'url' => '/'
        )
     )
   )
);

You could easily achieve this structure by using a Model as well. You will need function child_items or something as we will render the menu from the top down, and not from the bottom up.

Now in my master blade template I do this:

<ul>
    @foreach ($menu as $item)
       @include('layouts._menuItem', array('item' => $mainNavItem))
    @endforeach
</ul>

And then in the layouts._menuItem template I do this:

<?php
$items = array_key_exists('items', $item) ? $item['items'] : false;
$name = array_key_exists('name', $item) ? $item['name'] : '';
$url = array_key_exists('url', $item) ? url($item['url']) : '#';
$active = array_key_exists('url', $item) ? Request::is($item['url'].'/*') : false;
?>

<li class="@if ($active) active @endif">
       <a href="{{ $url }}">{{ $name }}</a>
       @if ($items)
          <ul>
             @foreach ($items as $item)
               @include('layouts._menuItem', array('item' => $item))
             @endforeach
          </ul>
       @endif
</li>

As you can see this template calls itself recursively, but with a different $item variable. This means can go as deep as you want in your menu structure. (The php block is just there to prepare some variables so I could keep the actual template code clean and readable, technically it is not required).

I stripped the Twitter Bootstrap code in the snippets above to keep things simple (I actually have titles, dropdown toggles, icons, dividers, ... in my template / array), so the code is not tested. The full version is working fine for me though, so let me know if I made a mistake somewhere.

Hope this helps you (or anyone else, cause this is a rather old question) on the way. Let me know if you need any more pointers / help, or if you want my full code.

Happy coding!

like image 87
Pevara Avatar answered Nov 13 '22 03:11

Pevara


I believe the following is the correct way to do recursion in laravel.

Supposing we have a children relation, you can add this to your model class:

public function getDescendants ( $parent= false ) {
    $parent = $parent ?: $this;
    $children = $parent->children()->get();

    foreach ( $children as $child ) {
        $child->setRelation(
            'children', 
            getDescendants( $child )
        );
    }

    return $children;
}

The above will get all the children records recursively, and you can access them like this:

$d = Category::find(1)->getDescendants();

foreach ( $d as $child_level_1 ) {
    foreach ( $child_level_1->children as $child_level_2 ) {
        foreach ( $child_level_2->children as $child_level_3 ) {
            // ...... this can go on for infinite levels
        }
    }
}

Although not tested, the following might be useful to flatten all the recursive relations into one collection of models (check the documentation on adding new methods to collections):

// Add this to your model
public function newCollection ( array $models = array() ) {
    return new CustomCollection( $models );
}


// Create a new file that extends the orginal collection
// and add the flattenRelation method
class CustomCollection extends Illuminate\Database\Eloquent\Collection {
    // Flatten recursive model relations
    public static function flattenRelation ( $relation ) {
        $collection = $this;
        // Loop through the collection models
        foreach ( $collection as $model ) {

            // If the relation exists
            if ( isset($model->relations[$relation]) ) {
                // Get it
                $sub_collection = $model->relations[$relation];

                // And merge it's items with the original collection
                $collection = $collection->merge(
                    $sub_collection->flatten($relation)
                );

                // Them remove it from the relations
                unset( $model->relations[$relation] );
            }

        }

        // Return the flattened collection
        return $collection;
    }
}

That way you can do the following:

// This will get the descenands and flatten them recursively
$d = Category::find(1)->getDescendants()->flattenRelation( 'children' );

// This will give you a flat collection of all the descendants
foreach ( $d as $model ) {

}
like image 21
ktsakas Avatar answered Nov 13 '22 04:11

ktsakas


My laravel menu with unlimited submenus (menu items from database)

public function CreateMenu( $parid, $menu, $level ) {

    $output = array();    
    $action= Route::current()->getUri();
    $uri_segments = explode('/', $action);
    $count=count($uri_segments);
    foreach( $menu as $item => $data ) {

            if ($data->parent_id == $parid) { 
                $uri='';
                $output[ $data->id ] = $data;
                for($i=0; $i<=$level; $i++) {
                    if($i < $count) {
                        $uri.="/".Request::segment($i+1);
                    }
                    if($uri == $data->link ) {
                        $output[ $data->id ]->activeClass = 'active';
                        $output[ $data->id ]->inClass = 'in';
                    }
                    else {
                        $output[ $data->id ]->activeClass = '';
                        $output[ $data->id ]->inClass = '';
                    }
                    $output[ $data->id ]->level = $level+2;
                }
                $output[ $data->id ]->submenu = self::CreateMenu( $data->id, $menu, $level+1 );
            }

    }
    return $output;

}

In the BaseController or where you want, put

$navitems=DB::table('navigations')->get();
$menu=BaseController::CreateMenu(0,$navitems,0);
return View::share($menu);

After that, i put the menu html to the macro.php

HTML::macro('MakeNavigation', function($data) {

foreach ($data as $key => $value) {
    if($value->submenu) {
        echo '<li class="'.$value->activeClass.'">
        <a href="'.$value->link.'" class="'.$value->activeClass.'">"'
            .$value->name.' <span class="fa arrow"></span> 
        </a>';
        echo "<ul class='nav nav-".$value->level."-level ".$value->inClass." '>";
            HTML::MakeNavigation($value->submenu);
        echo "</ul>";
    }
    else {
        echo '<li class="'.$value->activeClass.'">
        <a href="'.$value->link.'" class="'.$value->activeClass.'">'
                      .$value->name.'
        </a>';
    }
    echo "</li>";
}});

And in the view (blade templating) just call the

{{ HTML::MakeNavigation($menu) }}
like image 23
user3229469 Avatar answered Nov 13 '22 03:11

user3229469