XXXIV. Chapitre 34 : Les relations avec Eloquent 2/2▲
Modification le 5/5/2014 : quelques changements dans l'organisation du code et dans la syntaxe (en particulier si vous aviez des soucis avec le PSR-0 ça devrait maintenant être réglé).
Dans le précédent chapitreprécédent chapitrechapitreChapitre 33 : Les relations avec Eloquent 1/2 j'ai détaillé les possibilités relationnelles d'Eloquent. Maintenant il nous reste à voir comment gérer tout ça. Le voyage va être parfois un peu mouvementé, alors accrochez-vous…
XXXIV-A. La base▲
La base de référence qui va nous servir est la même que celle que nous avons vue précédemment :
XXXIV-B. Le code▲
XXXIV-B-1. Installation▲
Comme le code est volumineux je ne vais pas le mettre complètement ici mais juste m'y référer. Vous pouvez le télécharger ici. Il vous suffit de caser tout ça dans un dossier et de faire :
- composer install ;
- créer une base et renseigner son nom dans app/config/database.php ;
- php artisan migrate:install ;
- php artisan migrate ;
- php artisan db:seed.
Vous devriez alors avoir une application fonctionnelle et arriver sur cette page :
XXXIV-B-2. Organisation du code▲
Voici l'architecture des dossiers dans app :
Vous pouvez remarquer la présence du dossier Lib qui contient pratiquement tout le code de l'application et le fichier macro.php qui contient des macros HTML et Form. Vous pouvez aussi voir qu'il n'y a pas les dossiers models, views et controllers. Je suis parti sur une organisation du code fondée sur des entités, en l'occurrence les tables. Si vous regardez le contenu du dossier Lib :
Vous voyez un dossier pour presque chacune des tables de la base. Le code correspondant à la gestion de chaque table se trouve dans ce dossier. Par exemple pour les villes :
Vous trouvez ici les vues, le modèle, le contrôleur, la validation et la gestion. c'est la même chose pour chaque table.
Il y a aussi un dossier Commun qui contient tout le code qui concerne toutes les gestions :
Ici on a les templates, la validation (la classe utilisée est celle du bouquin de Fidao Implementing Laravel), les vues communes, la classe abstraite des contrôleurs.
Cette organisation me paraît plus cohérente quand on commence à avoir pas mal de code. Il m'a fallu évidemment expliquer à Laravel où trouver tout ce code. Je l'ai fait au niveau de Composer :
"psr-0"
:
{
"Lib"
:
"app"
}
Pour les vues il m'a fallu changer le path dans app/config/view.php :
'
paths
'
=>
array(__DIR__.
'
/../Lib
'
),
XXXIV-B-3. Les macros▲
Pour simplifier les vues avec l'utilisation de bootstrap j'ai créé quelques macros situées dans le fichier app/macros.php. J'ai renseigné le fichier app/start/global.php :
require app_path().
'
/macros.php
'
;
Par exemple pour faire apparaître une zone de texte avec son étiquette version bootstrap :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
Form::
macro('
boottext
'
,
function($name
,
$label
,
$input
=
''
)
{
return sprintf('
<div class="row">
<div class="form-group">
%s
<div class="col-md-10">
%s
</div>
</div>
</div>
'
,
Form::
label($name
,
$label
,
array("
class
"
=>
"
col-md-2
"
)),
Form::
text($name
,
$input
,
array('
class
'
=>
'
form-control
'
))
);
}
);
Ce qui génère en une seule ligne ce genre de code :
XXXIV-B-4. Les routes▲
J'ai traité tous les contrôleurs comme des ressources, du coup les routes sont très épurées :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
Route::
get('
/
'
,
function() {
return View::
make('
Commun.vues.accueil
'
);
}
);
Route::
resource('
villes
'
,
'
Lib\Villes\VilleController
'
);
Route::
resource('
pays
'
,
'
Lib\Pays\PaysController
'
);
Route::
resource('
auteurs
'
,
'
Lib\Auteurs\AuteurController
'
);
Route::
resource('
livres
'
,
'
Lib\Livres\LivreController
'
);
Route::
resource('
editeurs
'
,
'
Lib\Editeurs\EditeurController
'
);
Route::
resource('
autoedites
'
,
'
Lib\Autoedites\AutoediteController
'
);
Route::
resource('
themes
'
,
'
Lib\Themes\ThemeController
'
);
Route::
resource('
categories
'
,
'
Lib\Categories\CategorieController
'
);
Route::
resource('
periodes
'
,
'
Lib\Periodes\PeriodeController
'
);
XXXIV-B-5. Les contrôleurs▲
Comme le traitement dans le cas des contrôleurs est similaire à celui des ressources, j'ai créé une classe abstraite qui contient l'essentiel du code :
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.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
<?php
namespace
Lib\Commun;
use
Illuminate\Support\Facades\View;
use
Illuminate\Support\Facades\Redirect;
abstract
class
BaseResourceController extends
\Illuminate\Routing\Controller {
protected
$gestion
;
protected
$base
;
protected
$message_store
;
protected
$message_update
;
public
function
__construct
()
{
$this
->
beforeFilter('csrf'
,
array
('on'
=>
array
('post'
,
'delete'
,
'put'
)));
$this
->
beforeFilter('ajax'
,
array
('on'
=>
array
('delete'
,
'put'
)));
}
public
function
index()
{
$lignes
=
$this
->
gestion->
listePages(10
);
return
View::
make($this
->
base.
'.vues.liste'
,
compact('lignes'
));
}
public
function
create()
{
return
View::
make($this
->
base.
'.vues.create'
,
$this
->
gestion->
create());
}
public
function
store()
{
$return
=
$this
->
gestion->
store();
if
($return
===
true
) {
return
Redirect::
route($this
->
base.
'.index'
)->
with('message_success'
,
$this
->
message_store);
}
return
Redirect::
route($this
->
base.
'.create'
)->
withInput()->
withErrors($return
);
}
public
function
show($id
)
{
return
View::
make($this
->
base.
'.vues.show'
,
$this
->
gestion->
show($id
));
}
public
function
edit($id
)
{
return
View::
make($this
->
base.
'.vues.edit'
,
$this
->
gestion->
edit($id
));
}
public
function
update($id
)
{
$return
=
$this
->
gestion->
update($id
);
if
($return
===
true
) {
return
Redirect::
route($this
->
base.
'.index'
)->
with('message_success'
,
$this
->
message_update);
}
return
Redirect::
route($this
->
base.
'.edit'
,
$id
)->
withInput()->
withErrors($return
);
}
public
function
destroy($id
)
{
$this
->
gestion->
destroy($id
);
return
Redirect::
back();
}
}
On se retrouve ainsi avec des contrôleurs très allégés.
XXXIV-B-6. Considérations générales▲
J'ai simplifié au maximum la situation pour me concentrer sur les procédures. Chaque table a un seul champ, je ne fais pas de tri pour l'affichage, etc.
J'ai prévu une barre de débogage pour voir les requêtes générées par Eloquent.
XXXIV-C. Gestion des pays▲
On va commencer avec un cas simple, celui des pays. Au niveau des relations la table des pays est reliée à la table des villes par un lien 1:n :
On a vu également qu'on peut atteindre les auteurs avec la méthode hasManyThrough.
XXXIV-C-1. Le modèle▲
Comme on a des espaces de noms, il faut évidemment bien renseigner le code en conséquence pour qu'Eloquent s'y retrouve :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
<?php
namespace
Lib\Pays;
use
Eloquent;
class
Pays extends
Eloquent {
protected
$table
=
'pays'
;
public
$timestamps
=
true
;
protected
$softDelete
=
false
;
protected
$guarded
=
array
('id'
);
public
function
villes()
{
return
$this
->
hasMany('\Lib\Villes\Ville'
);
}
public
function
auteurs()
{
return
$this
->
hasManyThrough('\Lib\Auteurs\Auteur'
,
'\Lib\Villes\Ville'
);
}
}
Ce sera la même chose pour tous les modèles de l'application.
XXXIV-C-2. Le contrôleur▲
J'ai parlé plus haut de la classe abstraite pour les contrôleurs, et ça se traduit pour celui des pays à très peu de code :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
<?php
namespace
Lib\Pays;
class
PaysController extends
\Lib\Commun\BaseResourceController {
public
function
__construct
(PaysGestion $gestion
)
{
parent
::
__construct
();
$this
->
gestion =
$gestion
;
$this
->
base =
class_basename(__NAMESPACE__);
$this
->
message_store =
'Le pays a été ajouté'
;
$this
->
message_update =
'Le pays a été modifié'
;
}
}
La gestion est déléguée à la classe PaysGestion qui est injectée dans le modèle. On a ainsi une bonne séparation des tâches.
XXXIV-C-3. La liste▲
Pour obtenir la liste des pays on passe par la méthode ListePages de la classe abstraite BaseGestion commune à toutes les gestions :
Cette méthode est appelée par la méthode index du contrôleur de base :
On voit ici qu'on appelle la vue liste pour la table concernée, ici celle des pays. Le code des vues pour les listes est similaire pour toutes les tables, voici ce que ça donne pour les pays :
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.
@extends
('Commun.templates.liste'
)
@section
('entete'
)
Liste des pays
{{
link_to_route('pays.create'
,
'Ajouter un pays'
,
null
,
array
('class'
=>
'btn btn-info pull-right'
)) }}
@stop
@section
('titre'
)
Pays
@stop
@section
('tableau'
)
@foreach
($lignes
as
$ligne
)
<
tr>
<
td>
{{
$ligne
->
id }}
<
/td
>
<
td class
=
"
text-primary
"
><strong>
{{
$ligne
->
nom }}
<
/strong
></td
>
<
td>
{{
link_to_route('pays.show'
,
'Voir'
,
array
($ligne
->
id),
array
('class'
=>
'btn btn-success btn-block'
)) }}
<
/td
>
<
td>
{{
link_to_route('pays.edit'
,
'Modifier'
,
array
($ligne
->
id),
array
('class'
=>
'btn btn-warning btn-block'
)) }}
<
/td
>
<
td>
{{
Form::
open(array
('method'
=>
'DELETE'
,
'route'
=>
array
('pays.destroy'
,
$ligne
->
id))) }}
{{
Form::
submit('Supprimer'
,
array
('class'
=>
'btn btn-danger btn-block'
,
'onclick'
=>
'return confirm(
\'
Vraiment supprimer cet enregoistrement ?
\'
)'
)) }}
{{
Form::
close() }}
<
/td
>
<
/tr
>
@endforeach
@stop
Je ne rentre pas dans le détail de ce code ni dans le template correspondant parce que ce n'est pas le sujet. Voici l'aspect de la page obtenue :
La pagination est réglée à dix enregistrements. Pour chaque ligne on peut voir le pays, le modifier ou le supprimer.
XXXIV-C-4. La fiche du pays▲
Pour voir un pays c'est la méthode show de la gestion qui est appelée :
La méthode find permet de récupérer l'enregistrement à partir de son ID qui a été renseigné dans la vue. Ensuite on prévoit les informations nécessaires pour la fiche du pays :
- évidemment les champs du pays $pays ;
- les villes du pays récupérées avec $pays->villes (méthode hasMany du modèle) ;
- les auteurs du pays récupérés avec $pays->auteurs(méthode hasManyThrough du modèle).
La vue récupère ces informations :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
@extends
('Commun.templates.fiche'
)
@section
('titre'
)
<
h1>
Fiche de pays<
/h1
>
@stop
@section
('contenu'
)
{{
HTML::
bootpanel('Nom du pays'
,
$pays
->
nom) }}
{{
HTML::
bootpanelmulti('Villes'
,
$villes
,
'nom'
) }}
{{
HTML::
bootpanelmulti('Auteurs'
,
$auteurs
,
'nom'
) }}
@stop
L'utilisation de macros (situées dans le fichier app/macros.php) permet de rendre le code simple et lisible. Résultat obtenu :
XXXIV-C-5. Modifier un pays▲
La modification d'un pays est simple parce que ça n'impacte pas la relation avec les villes. Au niveau de la gestion on se contente de récupérer l'enregistrement :
Et dans la vue on affiche le seul champ disponible (toujours avec les macros) :
2.
3.
4.
5.
6.
7.
8.
9.
10.
@extends
('Commun.templates.form'
)
@section
('formulaire'
)
{{
Form::
open(array
('url'
=>
'pays/'
.
$pays
->
id,
'method'
=>
'put'
,
'class'
=>
'form-horizontal panel'
)) }}
@
include ('commun.templates.messages'
)
{{
Form::
bootlegend('Modification du pays'
) }}
{{
Form::
boottext('nom'
,
'Nom :'
,
$pays
->
nom) }}
{{
Form::
bootbuttons(url('pays'
)) }}
{{
Form::
close() }}
@stop
Ce qui donne cet aspect :
Au retour on teste la validité, on sauvegarde avec la méthode save, et on retourne :
La redirection a lieu dans le contrôleur de base :
La création est calquée sur ce modèle, je ne la présente donc pas.
XXXIV-C-6. Détruire un pays▲
N'ayez pas peur, rien de belliqueux là-dedans. Étant donné qu'il y a dans la table des villes une clé étrangère avec l'identifiant du pays (en fait autant de clés que de pays en relation), on ne va pas supprimer un pays sans précaution, au risque d'avoir une clé étrangère qui ne se réfère plus à rien du tout. Un petit schéma pour bien voir ça :
Si je ne veux pas que le champ pays_id dans la table villes devienne orpheline, j'ai le choix :
- je peux demander à MySQL (ou autre) de faire une suppression en cascade. Dans ce cas si je supprime le pays, les villes sont aussi automatiquement supprimées dans la base (et ainsi de suite s'il y a une autre cascade). C'est la méthode radicale qu'en général on évite ;
- je peux demander à MySQL de m'interdire la suppression du pays. Du coup, si je le fais je reçois une erreur de sa part. C'est l'attitude par défaut, et c'est celle que j'ai laissée.
Du coup dans le traitement de la suppression je vais vérifier s'il existe encore au moins une ville en relation :
S'il y a au moins une ville j'envoie un message, sinon je supprime le pays.
XXXIV-D. Gestion des villes▲
Voyons à présent la gestion des villes. Il y a pas mal de similitude avec celle des pays, je vais donc m'attacher à montrer les particularités.
Au niveau des relations on a une double liaison :
Un ville appartient à un pays (relation 1:n vue du côté n) et une ville possède plusieurs auteurs (relation 1:n vue du côté 1). Qu'est-ce que cela implique ?
Pour l'affichage de la liste il n'y a aucune différence avec les pays. Pour la suppression c'est pareil, on va faire attention parce que la table auteurs possède la clé étrangère ville_id. Je ne reviens donc pas sur ces deux points, il suffit de vous reporter à la gestion des pays vue ci-dessus.
XXXIV-D-1. La fiche de la ville▲
Dans la fiche de la ville on va afficher le pays auquel elle appartient et tous les auteurs qui l'habitent. Ce qui donne ce code de gestion :
Et cette vue :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
@extends
('Commun.templates.fiche'
)
@section
('titre'
)
<
h1>
Fiche de ville<
/h1
>
@stop
@section
('contenu'
)
{{
HTML::
bootpanel('Nom de la ville'
,
$ville
->
nom) }}
{{
HTML::
bootpanel('Nom du pays'
,
$pays
) }}
{{
HTML::
bootpanelmulti('Auteurs'
,
$auteurs
,
'nom'
) }}
@stop
Et cet aspect :
XXXIV-D-2. Modifier une ville▲
La table ville comporte deux champs :
- nom : c'est le nom de la ville ;
- pays_id : c'est la clé étrangère qui se réfère au pays.
Il faut donc permettre de modifier ces deux valeurs. Pour le nom c'est simple, un simple contrôle de texte fait l'affaire. Pour le pays c'est plus délicat, il faut proposer un choix parmi tous les pays, donc prévoir une liste de choix avec tous les noms de pays. Voici la gestion :
La variable $ville contiendra les informations de la table villes et la variable $select contiendra tous les pays avec leur nom et leur id. Avec les macros la vue est épurée :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
@extends
('Commun.templates.form'
)
@section
('formulaire'
)
{{
Form::
open(array
('url'
=>
'villes/'
.
$ville
->
id,
'method'
=>
'put'
,
'class'
=>
'form-horizontal panel'
)) }}
@
include ('commun.templates.messages'
)
{{
Form::
bootlegend('Modification de la ville'
) }}
{{
Form::
boottext('nom'
,
'Nom :'
,
$ville
->
nom) }}
{{
Form::
bootselect('pays_id'
,
'Pays :'
,
$select
,
$ville
->
pays_id) }}
{{
Form::
bootbuttons(url('villes'
)) }}
{{
Form::
close() }}
@stop
On obtient ce formulaire avec le nom de la ville et la liste de choix avec le bon pays sélectionné :
Au retour on teste la validité et on enregistre :
XXXIV-D-3. Créer une ville▲
Pour créer une ville c'est pratiquement la même chose pour le formulaire, à part que le nom est vierge et qu'au retour il faut créer une nouvelle entité :
XXXIV-E. Gestion des livres▲
Avec la gestion des livres on attaque un gros morceau. Voici les trois relations :
- avec les éditeurs et les autoéditeurs on a une relation polymorphique de type 1:n vue du côté n ;
- avec les thèmes on a une relation de type 1:n vue du côté n ;
- avec les auteurs on a une relation de type n:n.
Nous avons déjà vu le deuxième cas avec les villes, le traitement sera donc identique. Par contre nous avons deux nouveaux cas à analyser attentivement.
Pour la relation avec les auteurs de type n:n on sait qu'il nous faut une table pivot, ici c'est auteur_livre. On part du principe qu'un livre a au moins un auteur et une quantité maximale indéterminée d'auteurs.
Pour la relation polymorphique on sait qu'un livre appartient soit à un éditeur, soit à un autoéditeur.
XXXIV-E-1. La fiche du livre▲
La fiche du livre doit comporter :
- le titre du livre ;
- le thème ;
- le nom de l'éditeur ou de l'autoéditeur ;
- les noms des auteurs.
Voici la méthode show :
On a quatre variables transmises pour les quatre informations nécessaires. Voici la vue :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
@extends
('Commun.templates.fiche'
)
@section
('titre'
)
<
h1>
Fiche de livre<
/h1
>
@stop
@section
('contenu'
)
{{
HTML::
bootpanel('Titre du livre'
,
$livre
->
titre) }}
{{
HTML::
bootpanel('Thème'
,
$theme
) }}
{{
HTML::
bootpanel('Editeur'
,
$editeur
) }}
{{
HTML::
bootpanelmulti('Auteurs'
,
$auteurs
,
'nom'
) }}
@stop
Et le résultat :
Vous remarquez comment Eloquent permet de faire ça avec simplicité.
XXXIV-E-2. Modifier un livre▲
Maintenant envisageons la modification d'un livre. Cette fois je vais partir à l'envers. On veut le formulaire suivant :
Avec ces possibilités :
- pour le titre : un simple contrôle de texte ;
- pour le thème : une liste de choix avec tous les thèmes et au départ le bon thème sélectionné ;
- pour les auteurs : au départ autant de listes de choix que d'auteurs avec le bon auteur chaque fois sélectionné, un bouton de suppression pour chaque auteur, et un bouton pour ajouter un auteur, on aura donc là un traitement dynamique du formulaire ;
- pour les éditeurs : deux boutons « radio » pour choisir entre les deux possibilités avec le bon bouton sélectionné au départ, et une liste de choix avec - selon le bouton radio sélectionné - les éditeurs ou les autoéditeurs, avec évidemment le bon sélectionné au départ.
Voici la gestion correspondante :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
public function edit($id
)
{
$old
=
Input::
old();
$livre
=
$this
->
model->
find($id
);
if(empty($old
))
{
$livrable
=
$livre
->
livrable_type ==
'
Lib\Editeurs\Editeur
'
;
}
else {
$livrable
=
$old
[
'
livrable
'
]
==
'
Lib\Editeurs\Editeur
'
;
}
$retour
=
array(
'
livre
'
=>
$livre
,
'
select_theme
'
=>
$this
->
theme->
all()->
lists('
nom
'
,
'
id
'
),
'
select_auteurs
'
=>
$this
->
auteur->
all()->
lists('
nom
'
,
'
id
'
),
'
select_editeurs
'
=>
$this
->
editeur->
all()->
lists('
nom
'
,
'
id
'
),
'
select_autoedites
'
=>
$this
->
autoedite->
all()->
lists('
nom
'
,
'
id
'
),
'
livrable
'
=>
$livrable
);
$retour
[
'
auteurs
'
]
=
empty($old
) ?
$livre
->
auteurs->
toArray():
$old
[
'
auteur
'
];
return $retour
;
}
On va regarder tout ça de près. Déjà au niveau des informations transmises :
- pour tous les champs du livre c'est tout simple, on les a directement dans le modèle ;
-
pour remplir les listes des thèmes, des auteurs, des éditeurs et des autoéditeurs, on envoie les quatre paquets avec :
Sélectionnez1.
2.
3.
4.'
select_theme
'
=>
$this
->
theme->
all()->
lists('
nom
'
,
'
id
'
),
'
select_auteurs
'
=>
$this
->
auteur->
all()->
lists('
nom
'
,
'
id
'
),
'
select_editeurs
'
=>
$this
->
editeur->
all()->
lists('
nom
'
,
'
id
'
),
'
select_autoedites
'
=>
$this
->
autoedite->
all()->
lists('
nom
'
,
'
id
'
),
-
pour savoir si c'est éditeur ou autoéditeur il y a plusieurs façons de faire. J'ai choisi de transmettre une valeur booléenne qui indique qu'il s'agit des éditeurs si elle est vraie, et évidemment l'inverse si elle est fausse ;
- pour les auteurs, on les trouve facilement avec la relation $livre->auteurs.
Le traitement spécifique des auteurs et des éditeurs est nécessaire en cas d'échec de validation. Il faut dans ce cas renvoyer à l'utilisateur ses choix en générant les bons contrôles. C'est la raison du test de présence d'anciennes entrées, qui servent de référence dans ce cas.
La vue est évidemment un peu chargée pour traiter tout ça :
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.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
@extends
('Commun.templates.form'
)
@section
('formulaire'
)
{{
Form::
open(array
('url'
=>
'livres/'
.
$livre
->
id,
'method'
=>
'put'
,
'class'
=>
'form-horizontal panel'
)) }}
@
include ('commun.templates.messages'
)
{{
Form::
bootlegend('Modification du livre'
) }}
<
br>
{{
Form::
boottext('titre'
,
'Titre :'
,
$livre
->
titre) }}
<
br>
{{
Form::
bootselect('theme_id'
,
'Theme :'
,
$select_theme
,
$livre
->
theme_id) }}
<
br>
@for
($i
=
0
;
$i
<
count($auteurs
);
$i
++
)
{{
Form::
bootselectbutton('auteur'
,
$i
,
'Auteur :'
,
$select_auteurs
,
$auteurs
[
$i
]
) }}
@endfor
<
div class
=
"
row
"
>
<
button id
=
"
add
"
type
=
"
button
"
class
=
"
btn btn-primary pull-right
"
>
Ajouter un auteur<
/button
>
<
/div
>
<
br>
<
div class
=
"
row form-group
"
>
<
label class
=
"
radio-inline
"
>
{{
Form::
radio('livrable'
,
'Lib\Editeurs\Editeur'
,
$livrable
) }}
Editeur
<
/label
>
<
label class
=
"
radio-inline
"
>
{{
Form::
radio('livrable'
,
'Lib\Autoedites\Autoedite'
,
!
$livrable
) }}
Auto Editeur
<
/label
>
<
/div
>
<
div class
=
"
toggle
{{
$livrable
?
'show'
:
'hidden'
}}
"
>
{{
Form::
bootselect('editeur_id'
,
'Editeur :'
,
$select_editeurs
,
$livre
->
livrable_id) }}
<
/div
>
<
div class
=
"
toggle
{{
$livrable
?
'hidden'
:
'show'
}}
"
>
{{
Form::
bootselect('autoedite_id'
,
'Auto Editeur :'
,
$select_autoedites
,
$livre
->
livrable_id) }}
<
/div
>
<
br><hr>
{{
Form::
bootbuttons(url('livres'
)) }}
{{
Form::
close() }}
@stop
@section
('scripts'
)
<script>
$(
function
(
){
// Suppression d'une ligne d'auteurs
$(
"
.btn-danger
"
).click
(
function
(
) {
// On supprime la ligne s'il en reste au moins 2
if
(
$(
'
.ligne
'
).
length >
1
) $(
this
).parents
(
'
.row .ligne
'
).remove
(
);
}
);
// Ajout d'une ligne d'auteurs
$(
"
#add
"
).click
(
function
(
) {
// Recherche dernier id
var
max =
id =
0
;
$(
'
.ligne
'
).each
(
function
(
){
id =
parseInt
((
$(
this
).attr
(
'
id
'
)).substring
(
11
));
if
(
id >
max) max =
id;
}
);
// Première ligne
var
clone =
$(
'
#ligneauteur
'
+
max).clone
(
true
);
// Change l'id
clone.attr
(
'
id
'
,
'
ligneauteur
'
+
++
max);
// Change le for du label
clone.find
(
'
label
'
).attr
(
'
for
'
,
'
auteur
'
+
max);
// Change l'id du select
clone.find
(
'
select
'
).attr
(
'
id
'
,
'
auteur
'
+
max);
// Ajoute la ligne à la fin
$(
'
#ligneauteur
'
+
id).after
(
clone);
}
);
// Changement editeur/auto editeur
$(
'
input[type="radio"]
'
).change
(
function
(
) {
$(
'
.toggle
'
).toggleClass
(
'
show hidden
'
);
}
);
}
)
</
script>
@stop
Je ne détaille pas tout ce code et il est sans doute améliorable, je n'ai pas cherché à l'optimiser. Je précise juste que je gère l'aspect dynamique avec jQuery, en clonant le contrôle des auteurs puisqu'il doit en rester au moins un, et en jouant avec des classes pour les éditeurs.
Le traitement au retour est assez facile avec Eloquent :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
public function update($id
)
{
if($this
->
validation->
with(Input::
all())->
passes())
{
$livre
=
$this
->
model->
find($id
);
DB::
transaction(function() use($livre
)
{
$livre
->
auteurs()->
sync(Input::
get('
auteur
'
));
$livre
->
titre =
Input::
get('
titre
'
);
$livre
->
theme_id =
Input::
get('
theme_id
'
);
$type
=
Input::
get('
livrable
'
);
$livre
->
livrable_type =
$type
;
$livre
->
livrable_id =
($type
==
'
Lib\Editeurs\Editeur
'
) ?
Input::
get('
editeur_id
'
) :
Input::
get('
autoedite_id
'
);
$livre
->
save();
}
);
return true;
}
return $this
->
validation->
errors();
}
Notez la synchronisation élégante de la table pivot auteur_livre avec cette simple ligne de code :
$livre
->
auteurs()->
sync(Input::
get('
auteur
'
));
XXXIV-E-3. Transaction ?▲
Vous avez sans doute remarqué que j'ai placé le code de la mise à jour du livre dans une fonction anonyme pour la transaction. Mais qu'est-ce qu'une transaction ? Dans la mise à jour du livre on fait deux choses :
- on met à jour la table pivot pour référencer les auteurs ;
- on met à jour la table des livres.
Imaginez que la première action se fasse et pas la seconde. On aurait la moitié de ce que l'on veut qui serait exécuté. Dans le cas présent ça ne pose aucun problème au niveau intégrité de la base, mais ce n'est quand même pas une situation souhaitable parce qu'on ne sait plus où on en est. L'attitude dans ce cas est de dire : on fait tout ou on ne fait rien. C'est l'objet d'une transaction. Le fait d'inclure les deux actions dans la transaction nous assure qu'elles seront soit exécutées toutes les deux, soit aucune des deux, mais pas à moitié.
XXXIV-E-4. Création d'un livre▲
Pour créer un livre le formulaire est identique, il est juste non renseigné au départ. Je ne détaille donc pas cette partie du code. La seule chose intéressante à noter est la manière de créer les lignes dans la table pivot. Voici la méthode store :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
public function store()
{
if($this
->
validation->
with(Input::
all())->
passes())
{
$livre
=
new $this
->
model;
$livre
->
theme_id =
Input::
get('
theme_id
'
);
$livre
->
titre =
Input::
get('
titre
'
);
$type
=
Input::
get('
livrable
'
);
$livre
->
livrable_type =
$type
;
$livre
->
livrable_id =
($type
==
'
Lib\Editeurs\Editeur
'
) ?
Input::
get('
editeur_id
'
) :
Input::
get('
autoedite_id
'
);
DB::
transaction(function() use($livre
)
{
$livre
->
save();
$auteurs
=
array_unique(Input::
get('
auteur
'
));
foreach($auteurs
as $auteur_id
)
{
$livre
->
auteurs()->
attach($auteur_id
);
}
}
);
return true;
}
return $this
->
validation->
errors();
}
Remarquez l'utilisation de la méthode attach pour ajouter les lignes dans la table pivot. Il faut encore utiliser une transaction parce qu'on pourrait se retrouver avec un livre sans auteur.
XXXIV-E-5. Destruction d'un livre▲
Pour détruire un livre il n'y a pas de précaution particulière à prendre, si ce n'est de détruire également les lignes de la table pivot :
Cela se fait avec la méthode detach sans paramètre pour les effacer tous. Il faut le faire avant la suppression du livre pour respecter l'intégrité référentielle de la base et ne pas tomber sur une erreur. On aurait pu aussi dire à MySQL de faire une suppression en cascade, mais c'est toujours un peu risqué. On utilise encore une transaction puisque nous avons deux actions.
XXXIV-F. Gestion des éditeurs▲
On va souffler un peu avec les éditeurs qui ne présentent pas de difficultés, si ce n'est une relation de type 1:1 avec les contacts. et une relation polymorphique avec les livres :
Pour renseigner la fiche des éditeurs il faut aller récupérer le téléphone dans la table de contacts. On va chercher aussi les livres de l'éditeur sélectionné pour les afficher :
De la même façon pour la modification d'un éditeur il faut aller chercher le téléphone :
Au retour on doit mettre à jour les deux tables :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
public function update($id
)
{
if($this
->
validation->
with(Input::
all())->
passes())
{
$editeur
=
$this
->
model->
find($id
);
$editeur
->
nom =
Input::
get('
nom
'
);
$editeur
->
contact->
telephone =
Input::
get('
telephone
'
);
DB::
transaction(function() use($editeur
)
{
$editeur
->
contact->
save();
$editeur
->
save();
}
);
return true;
}
return $this
->
validation->
errors();
}
Encore une fois une transaction est nécessaire.
Pour créer un nouvel éditeur on a ce code :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
public function store()
{
if($this
->
validation->
with(Input::
all())->
passes())
{
$editeur
=
new $this
->
model;
$editeur
->
nom =
Input::
get('
nom
'
);
$contact
=
new $this
->
contact;
$contact
->
telephone =
Input::
get('
telephone
'
);
DB::
transaction(function() use($editeur
,
$contact
)
{
$editeur
->
save();
$editeur
->
contact()->
save($contact
);
}
);
return true;
}
return $this
->
validation->
errors();
}
Remarquez qu'on n'a pas besoin de renseigner la clé étrangère editeur_id, Eloquent s'en charge pour nous.
XXXIV-G. Gestion des thèmes▲
Voyons à présent la gestion des thèmes. Voyons la situation :
La table des thèmes possède une triple relation (une de type 1:n et deux polymorphiques de type n:n) :
- hasMany avec la table des livres ;
- morphedByMany avec la table des catégories ;
- morphedByMany avec la table des périodes.
Comme on l'a vu dans le précédent chapitreChapitre 33 : Les relations avec Eloquent 1/2 la polymorphie de type n:n remplace plusieurs relations de type n:n. Au lieu d'avoir autant de tables pivots que de relations on en a une seule qui les regroupe. L'identification se fait avec le nom du modèle en relation en plus des deux ID. Un thème peut donc être en relation avec 0 ou plusieurs catégories et avec 0 ou plusieurs périodes.
XXXIV-G-1. La fiche du thème▲
Que nous faut-il comme information pour la fiche d'un thème ? Voici la gestion correspondante :
On récupère : le thème sélectionné, les livres, les catégories et les périodes. Au niveau de la vue on procède toujours avec les macros :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
@extends
('Commun.templates.fiche'
)
@section
('titre'
)
<
h1>
Fiche de thème<
/h1
>
@stop
@section
('contenu'
)
{{
HTML::
bootpanel('Nom du thème'
,
$theme
->
nom) }}
{{
HTML::
bootpanelmulti('Catégories'
,
$categories
,
'nom'
) }}
{{
HTML::
bootpanelmulti('Périodes'
,
$periodes
,
'nom'
) }}
{{
HTML::
bootpanelmulti('Livres'
,
$livres
,
'titre'
) }}
@stop
Dans ce cas on a trois panneaux multiples :
XXXIV-G-2. La modification d'un thème▲
Voici le formulaire qu'on veut :
Il se divise en trois parties :
- pour le nom un simple contrôle de texte ;
- pour les catégories des listes de choix avec traitement dynamique (de 0 à n listes) ;
- pour les périodes des listes de choix avec traitement dynamique (de 0 à n listes).
Voici le code qui renseigne ce formulaire :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
public function edit($id
)
{
$theme
=
$this
->
model->
find($id
);
$retour
=
array(
'
theme
'
=>
$theme
,
'
select_categories
'
=>
$this
->
categorie->
all()->
lists('
nom
'
,
'
id
'
),
'
select_periodes
'
=>
$this
->
periode->
all()->
lists('
nom
'
,
'
id
'
)
);
$old
=
Input::
old();
$retour
[
'
categories
'
]
=
empty($old
) ?
$theme
->
categories->
toArray():
$old
[
'
categorie
'
];
$retour
[
'
periodes
'
]
=
empty($old
) ?
$theme
->
periodes->
toArray():
$old
[
'
periode
'
];
return $retour
;
}
On prévoit d'envoyer les champs du thème ($theme) et les éléments de remplissage des listes ($select_categories et $select_periodes). Pour le nombre de listes de choix à afficher, ça dépend évidemment de la situation : envoi initial du formulaire ou nouveau remplissage après erreur de validation. D'où le test de présence d'anciennes données.
La vue est un peu chargée parce qu'elle comporte le traitement dynamique des listes de choix en jQuery :
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.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
@extends
('Commun.templates.form'
)
@section
('formulaire'
)
{{
Form::
open(array
('url'
=>
'themes/'
.
$theme
->
id,
'method'
=>
'put'
,
'class'
=>
'form-horizontal panel'
)) }}
@
include ('commun.templates.messages'
)
{{
Form::
bootlegend('Modification du thème'
) }}
{{
Form::
boottext('nom'
,
'Nom :'
,
$theme
->
nom) }}
<
br><hr>
<
div id
=
"
cats
"
>
@for
($i
=
0
;
$i
<
count($categories
);
$i
++
)
{{
Form::
bootselectbutton('categorie'
,
$i
,
'Catégorie :'
,
$select_categories
,
$categories
[
$i
]
) }}
@endfor
<
/div
>
<
div class
=
"
row
"
>
<
button id
=
"
add_cat
"
type
=
"
button
"
class
=
"
btn btn-primary pull-right
"
>
Ajouter une catégorie<
/button
>
<
/div
>
<
br><hr>
<
div id
=
"
pers
"
>
@for
($i
=
0
;
$i
<
count($periodes
);
$i
++
)
{{
Form::
bootselectbutton('periode'
,
$i
,
'Période :'
,
$select_periodes
,
$periodes
[
$i
]
) }}
@endfor
<
/div
>
<
div class
=
"
row
"
>
<
button id
=
"
add_per
"
type
=
"
button
"
class
=
"
btn btn-primary pull-right
"
>
Ajouter une période<
/button
>
<
/div
>
<
br><hr>
{{
Form::
bootbuttons(url('themes'
)) }}
{{
Form::
close() }}
@stop
@section
('scripts'
)
<script>
$(
function
(
){
// Nombre de catégories et périodes au départ
var
cat_number =
$(
'
#cats .ligne
'
).
length;
var
per_number =
$(
'
#pers .ligne
'
).
length;
// Suppression d'une ligne
$(
document
).on
(
'
click
'
,
'
.btn-danger
'
,
function
(
){
$(
this
).parents
(
'
.row .ligne
'
).remove
(
);
}
);
// Ajout d'une ligne de catégorie
$(
"
#add_cat
"
).click
(
function
(
) {
var
id =
'
categorie
'
+
cat_number;
var
html =
'
<div class="row ligne" id="ligne
'
+
id +
'
">\n<div class="form-group">\n
'
+
'
<label for="categorie
'
+
cat_number +
'
" class="col-md-3">Catégorie :</label>\n
'
+
'
<div class="col-md-7">\n
'
+
'
{{
Form::
select('categorie[]'
,
$select_categories
,
null
,
array
('class'
=>
'form-control'
,
'id'
=>
'id_temp'
)) }}
\n
'
+
'
</div>\n<div class="col-md-2">\n<button type="button" class="btn btn-danger">Supprimer</button>\n</div>\n</div>\n
'
;
++
cat_number;
$(
'
#cats
'
).append
(
html);
$(
'
#id_temp
'
).attr
(
'
id
'
,
id);
}
);
// Ajout d'une ligne de période
$(
"
#add_per
"
).click
(
function
(
) {
var
id =
'
periode
'
+
per_number;
var
html =
'
<div class="row ligne" id="ligne
'
+
id +
'
">\n<div class="form-group">\n
'
+
'
<label for="periode
'
+
per_number +
'
" class="col-md-3">Période :</label>\n
'
+
'
<div class="col-md-7">\n
'
+
'
{{
Form::
select('periode[]'
,
$select_periodes
,
null
,
array
('class'
=>
'form-control'
,
'id'
=>
'id_temp'
)) }}
\n
'
+
'
</div>\n<div class="col-md-2">\n<button type="button" class="btn btn-danger">Supprimer</button>\n</div>\n</div>\n
'
;
++
per_number;
$(
'
#pers
'
).append
(
html);
$(
'
#id_temp
'
).attr
(
'
id
'
,
id);
}
);
}
)
</
script>
@stop
Là encore on peut sans doute améliorer ce code mais il accomplit dignement sa tâche. J'ai utilisé la méthode select de Form pour générer une partie du JavaScript. Au retour il faut mettre à jour l'enregistrement :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
public function update($id
)
{
if($this
->
validation->
with(Input::
all())->
passes())
{
$theme
=
$this
->
model->
find($id
);
DB::
transaction(function() use($theme
)
{
$theme
->
categories()->
sync(Input::
get('
categorie
'
));
$theme
->
periodes()->
sync(Input::
get('
periode
'
));
$theme
->
nom =
Input::
get('
nom
'
);
$theme
->
save();
}
);
return true;
}
return $this
->
validation->
errors();
}
J'utilise à nouveau une transaction parce qu'il y a plusieurs actions. La méthode sync permet une mise à jour simplifiée de la table pivot.
XXXIV-G-3. Créer un thème▲
Pour créer un thème, le formulaire est le même que pour la mise à jour sans le renseignement initial. On a juste besoin de remplir les listes de choix :
Au retour il faut créer le thème :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
public function store()
{
if($this
->
validation->
with(Input::
all())->
passes())
{
$theme
=
new $this
->
model;
$theme
->
nom =
Input::
get('
nom
'
);
DB::
transaction(function() use($theme
)
{
$theme
->
save();
if(!
is_null(Input::
get('
categorie
'
))) $theme
->
categories()->
attach(array_unique(Input::
get('
categorie
'
)));
if(!
is_null(Input::
get('
periode
'
))) $theme
->
periodes()->
attach(array_unique(Input::
get('
periode
'
)));
}
);
return true;
}
return $this
->
validation->
errors();
}
Il faut évidemment encore une transaction. La création des lignes dans la table pivot se fait simplement avec la méthode attach.
XXXIV-G-4. Supprimer un thème▲
Pour supprimer un thème il faut évidemment supprimer les lignes correspondantes dans la table pivot :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
public function destroy($id
)
{
$theme
=
$this
->
model->
find($id
);
if($theme
->
livres->
count() ==
0
) {
DB::
transaction(function() use($theme
)
{
$theme
->
categories()->
detach();
$theme
->
periodes()->
detach();
$theme
->
delete();
}
);
}
else {
Session::
flash('
message_danger
'
,
'
Ce thème ne peut pas être supprimé parce qu
\'
il possède des livres !
'
);
}
}
On voit que ça se fait facilement avec la méthode detach, qui est l'opposée de la méthode attach vue pour la création. Le fait de ne pas mentionner de paramètre signifie qu'on veut tout supprimer (oui il faut le savoir, ce n'est pas précisé dans la documentation).