Je lisais récemment des articles sur la création de menus avec Laravel. Je n’y ai pas vraiment trouvé quelque chose pour me satisfaire. Alors je me suis penché sur la question. Les solutions possibles sont assez variées. Je me suis orienté vers un composeur de vues. Je vous donne le résultat de ma réflexion.

Les composeurs de vues

Mais d’abord c’est quoi un composeur de vue ? C’est une fonction anonyme ou une classe qui est appelée lorsqu’une vue est créée. Ça permet de préparer des données pour la vue de façon simple et systématique et de localiser le code à un seul endroit.

Créer un composeur est facile :

View::composer('navigation', function($view)
{
    $view->with('menu', Menu::all());
});

Avec ce code chaque fois que la vue navigation est créée elle reçoit la variable $menu avec les informations nécessaires.

On peut aussi utiliser une classe si on a plus de traitement à effectuer :

View::composer('navigation', 'MenuComposer');

Avec une classe constituée de cette manière :

class MenuComposer {

    public function compose($view)
    {
        $view->with('menu', Menu::all());
    }
}

Voilà pour la présentation générale. Venons en maintenant au menu.

Format du menu

La première question que je me suis posée est : sous quelle forme est-il judicieux de créer un menu ? Après quelques essais j’en suis venu à adopter le format JSON pour sa versatilité. Il est facile à manipuler autant en PHP qu’en Javascript et il est aussi très facile à transmettre.

Voici le menu qui va servir pour notre exemple :

{
	"Accueil" : {
		"type": "url",
		"value": "/"
	},
	"A propos" : {
		"A propos de moi" : {
			"type": "route",
			"value": "apropos"
		},
		"A propos du site" : {
			"type": "route",
			"value": "aproposite"
		}
	},
	"Gallerie" : {
		"type": "url",
		"value": "gallerie"
	},
	"Liens" : {
			"Quelques liens :" : {
			"type" : "header"
		},
		"Les lieux" : {
			"type": "route",
			"value": "lieux"
		},
		"Les activités" : {
			"type": "route",
			"value": "activites",
			"state": "disabled"
		},
		"divider1" : {
			"type": "divider"
		},
		"Les ressources" : {
			"type": "route",
			"value": "ressources"
		}
	}
}

La structure est simple, on a :

  • le nom de l’item,
  • son type (url, route, header ou divider),
  • sa valeur éventuelle (pour l’url ou la route),
  • son état éventuel (en fait juste s’il est désactivé)

Il suffit de mettre tout ça dans un fichier qu’on va nommer navigation.php. Mais où va-t-on le placer ?

Organisation du code

Fidèle à mes habitudes j’ai créé un dossier app/Lib :

img18

On va faire le tour de tous ces fichiers. Pour que Laravel connaisse ce dossier et les classes contenues j’en informe Composer :

"autoload": {
	"classmap": [
           ...
	],
	"psr-0": {
		"Lib": "app"
	}  
},

Après un petit dumpautoload ça devrait aller. Pour que Laravel soit au courant que je positionne les vues dans le dossier app/Lib/views je lui dis dans le fichier app/config/views.php :

'paths' => array(__DIR__.'/../Lib/views'),

Le menu créé plus haut trouve sa place dans le dossier Lib/menus.

Le composeur

Occupons-nous maintenant du composeur (Lib/composers/NavigationComposer.php):

<?php namespace Lib\Composers;

use Illuminate\Filesystem\Filesystem;
use Illuminate\View\View;

class NavigationComposer {

	protected $files;

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

  public function compose(View $view) {

    $name = $view->getName();
    $fileName = app_path().'/Lib/menus/'.$name.'.php';

    if($this->files->exists($fileName)) {
      $file = $this->files->get($fileName);
      $items = json_decode($file, true);
      $view->items = $items;
    }

  }

}

Je m’arrange pour que la vue et le menu correspondant aient le même nom. Il me suffit donc de récupérer ce nom et de déterminer le chemin qui y mène. La fonction json_decode permet de transformer le json en tableau. Tant qu’à faire j’ai utilisé la classe Filesystem de Laravel pour la manipulation du fichier plutôt que les fonctions de base de PHP. A la sortie je crée une variable $items pour la vue.

Pour que Laravel soit au courant que ce composeur existe et le relier à la vue il faut un fournisseur de service (Lib/composers/ComposerServiceProvider.php) :

<?php namespace Lib\Composers;
 
use Illuminate\Support\ServiceProvider;
 
class ComposerServiceProvider extends ServiceProvider {
 
  public function register()
  {
    $this->app->view->composer('navigation', 'Lib\Composers\NavigationComposer');
  }
 
}

Si j’avais plusieurs menus je les déclarerais tous ici.

Et évidemment il faut déclarer le service dans app/config/app.php :

                ...
		'Illuminate\Workbench\WorkbenchServiceProvider',
		'Lib\Composers\ComposerServiceProvider'
	),

Les vues

Le template

Il ne nous reste plus qu’à nous occuper des vues. D’abord le template (Lib/views/main.blade.php) :

<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ trans('main.mon_joli_site') }}</title>
    {{ HTML::style('https://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css') }}
    {{ HTML::style('https://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap-theme.min.css') }}
    <!--[if lt IE 9]>
      {{ HTML::style('https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js') }}
      {{ HTML::style('https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js') }}
    <![endif]-->
  </head>
  <body>
    @include('navigation')
    <div class="container">
      @yield('contenu')
    </div>
    {{ HTML::script('http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js') }}
    {{ HTML::script('http://netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js') }}
  </body>
</html>

La navigation

C’est la vue (Lib/views/navigation.blade.php) qui va utiliser la variable transmise par le composeur :

<nav class="navbar navbar-default" role="navigation">
  <div class="container">
    <div class="navbar-header">
      <a class="navbar-brand">Mon joli site</a>
    </div>
    <ul class="nav navbar-nav">
      @foreach($items as $key => $value)
        @if(isset($value['type']))
          <li {{ Request::is($value['value']) ? ' class="active"' : null }} {{ isset($value['state'])? 'class='.$v['state'] : null }} >
            @if($value['type'] == 'url')
              <a href="{{ URL::to($value['value']) }}">{{ $key }}</a>
            @else
              <a href="{{ URL::route($value['value']) }}">{{ $key }}</a>
            @endif
          </li>
        @else
          <li class="dropdown">
            <a class="dropdown-toggle" data-toggle="dropdown" href="#"> {{ $key }} <span class="caret"></span></a>
            <ul class="dropdown-menu">
              @foreach ($value as $k => $v)
                @if($v['type'] == 'header')
                  <li class="dropdown-header">{{ $k }}</li>
                @elseif($v['type'] == 'divider')
                  <li class="divider"></li>
                @else
                  <li {{ Request::is($v['value']) ? ' class="active"' : null }} {{ isset($v['state'])? 'class='.$v['state'] : null }} >
                    @if($v['type'] == 'url')
                      <a href="{{ URL::to($v['value']) }}">{{ $k }}</a>
                    @elseif($v['type'] == 'route')
                      <a href="{{ URL::route($v['value']) }}">{{ $k }}</a>
                    @endif
                  </li>
                @endif
              @endforeach
            </ul>
          </li>
        @endif
      @endforeach
    </ul>
  </div>
</nav>

La tableau transmis est parcouru et le menu construit progressivement. J’ai utilisé Bootstrap par facilité (les classes sont toutes prêtes) mais j’aurais pu procéder différemment. Je fais un test de l’URL en cours pour rendre actif l’item correspondant.

Les autres vues

Les autres vues sont toutes calquées sur le même modèle. Par exemple pour Lib/views/home.blade.php :

@extends('main')

@section('contenu')
	Accueil
@stop

Pour mes essais j’ai juste changé le texte pour les autres vues.

Les routes

Pour que tout fonctionne il nous manque plus que les routes :

Route::get('/', function() { return View::make('home'); });
Route::get('apropos', array('as' => 'apropos', function() { return View::make('apropos'); }));
Route::get('aproposite', array('as' => 'aproposite', function() { return View::make('aproposite'); }));
Route::get('gallerie', function() { return View::make('gallerie'); });
Route::get('lieux', array('as' => 'lieux', function() { return View::make('lieux'); }));
Route::get('ressources', array('as' => 'ressources', function() { return View::make('ressources'); }));
Route::get('activites', array('as' => 'activites', function() { return View::make('activites'); }));

J’ai un peu mélangé les url et les routes nommées pour les tests.

Le résultat

Avec l’url de base j’ai ce résultat :

img66

J’ai bien l’accueil actif et les sous-menus fonctionnent :

img67

Si je clique sur « Gallerie » :

img68

J’ai la bonne page qui s’affiche et l’item « Gallerie » devient actif.

L’item « Les activités » est bien désactivé comme demandé et j’ai bien le titre et le séparateur :

img69

Au final la structure du système est simple et fonctionnelle. Le fait d’avoir le menu en JSON permet de le créer de façon visuelle par exemple avec une interface en Javascript.

  1. momsdenice

    Salut,

    J’ai essayé de suivre ton tuto mais j’ai cette erreur :

    PHP Fatal error: Class ‘Lib/Composers/ComposerServiceProvider’ not found in /Applications/MAMP/htdocs/Framework/Lds/bootstrap/compiled.php on line 4214

    Merci de me dire d’ou vient l’erreur

    😉

    • Author bestmomo

      Salut,

      Tu as un problème d’auto-chargement des classes. Relance un composer dumpautoload. Si ça marche toujours pas essai de supprimer ton fichier compiled.php et de lancer un composer dumpautoload pour le recréer.

Laisser un commentaire