XXXV. Chapitre 35 : Les composeurs de vues▲
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.
XXXV-A. Les composeurs de vues▲
Mais d'abord qu'est-ce qu'un 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 :
2.
3.
4.
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 d'autres traitements à effectuer :
View::
composer('
navigation
'
,
'
MenuComposer
'
);
Avec une classe constituée de cette manière :
2.
3.
4.
5.
6.
7.
class MenuComposer {
public
function
compose($view
)
{
$view
->
with('menu'
,
Menu::
all());
}
}
Voilà pour la présentation générale. Venons-en maintenant au menu.
XXXV-B. 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
{
"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 ?
XXXV-C. Organisation du code▲
Fidèle à mes habitudes j'ai créé un dossier app/Lib :
On va faire le tour de tous ces fichiers. Pour que Laravel connaisse ce dossier et les classes contenues j'en informe Composer :
2.
3.
4.
5.
6.
7.
8.
"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 le 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.
XXXV-D. Le composeur▲
Occupons-nous maintenant du composeur (Lib/composers/NavigationComposer.php) :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
<?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. À la sortie je crée une variable $items pour la vue.
Pour que Laravel soit au courant que ce composeur existe et le relie à la vue, il faut un fournisseur de service (Lib/composers/ComposerServiceProvider.php) :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
<?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 :
2.
3.
4.
...
'
Illuminate\Workbench\WorkbenchServiceProvider
'
,
'
Lib\Composers\ComposerServiceProvider
'
),
XXXV-E. Les vues▲
XXXV-E-1. Le template▲
Il ne nous reste plus qu'à nous occuper des vues. D'abord le template (Lib/views/main.blade.php) :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
<!
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
>
XXXV-E-2. La navigation▲
C'est la vue (Lib/views/navigation.blade.php) qui va utiliser la variable transmise par le composeur :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
<
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
>
Le 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.
XXXV-E-3. Les autres vues▲
Les autres vues sont toutes calquées sur le même modèle. Par exemple pour Lib/views/home.blade.php :
2.
3.
4.
5.
@extends
('main'
)
@section
('contenu'
)
Accueil
@stop
Pour mes essais j'ai juste changé le texte pour les autres vues.
XXXV-F. Les routes▲
Pour que tout fonctionne il ne nous manque plus que les routes :
2.
3.
4.
5.
6.
7.
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.
XXXV-G. Le résultat▲
Avec l'URL de base j'ai ce résultat :
J'ai bien l'accueil actif et les sous-menus fonctionnent :
Si je clique sur « Gallerie » :
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 :
Finalement 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.