VII. Le polymorphisme▲
Lors des deux précédents chapitres on a vu les principales relations que nous offre Eloquent : hasMany et belongsTomany. Je ne vous ai pas parlé de la relation hasOne parce que c'est juste du hasMany limité à un seul enregistrement et elle est peu utilisée. Dans tous les cas qu'on a vus on considère deux tables en relation. Dans le présent chapitre on va envisager le cas où une table peut être en relation avec plusieurs autres tables, ce qui se nomme du polymorphisme.
VII-A. Un peu de théorie▲
VII-A-1. La relation 1:1 ou 1:n▲
On a vu cette relation, en voici une schématisation pour fixer les esprits :
On a a possède un b (hasOne) ou a possède plusieurs b (hasMany).
La réciproque : b est possédé par un a (belongsTo).
VII-A-2. La relation n:n▲
On a vu aussi cette relation, en voici une schématisation pour fixer les esprits :
On a a appartient à un ou plusieurs b (belongsToMany).
Et on a b appartient à un ou plusieurs a (belongsToMany).
VII-A-3. La relation une table vers plusieurs tables▲
VII-A-3-a. Type de relation 1:n▲
Maintenant imaginons cette situation :
La table c peut être en relation soit avec la table a, soit avec la table b. Dans cette situation comment gérer une clé étrangère dans la table c ? Comment l'appeler et comment savoir avec quelle table elle est en relation ?
On voit bien qu'il va falloir une autre information : connaître sûrement la table en relation.
Puisqu'on a besoin de deux informations il nous faut deux colonnes :
On a donc deux colonnes :
- relatable_id : la clé étrangère qui mémorise l'identifiant de l'enregistrement en relation ;
- relatable_type : la classe du modèle en relation.
Voici la figure complétée avec les noms de ces relations :
- morphOne : c'est le hasOne mais issu de plusieurs tables ;
- morphMany : c'est le hasMany mais issu de plusieurs tables ;
- morphTo : c'est le belongsTo mais à destination de plusieurs tables.
VII-A-3-b. Type de relation n:n▲
On peut avoir le même raisonnement pour une relation de type n:n avec plusieurs tables d'un côté de la relation :
- morphToMany : c'est le belongsToMany mais issu de plusieurs tables ;
- morphedByMany : c'est le belongsToMany mais en direction de plusieurs tables.
VII-B. L'application d'exemple▲
Maintenant qu'on a vu la théorie passons à la pratique avec un cas dans l'application d'exemple.
VII-B-1. Présentation▲
Quand vous allez dans l'administration vous tombez sur le tableau de bord :
Il y a des pavés qui indiquent le nombre de nouveaux contacts, articles, utilisateurs et commentaires. Chaque fois que l'une de ces entités est créée on le mémorise dans la base, dans la table ingoings :
Il y a deux colonnes intéressantes :
- ingoing_id ;
- ingoing_type.
Ça doit vous évoquer quelque chose par rapport à ce que j'ai expliqué ci-dessus sur le polymorphisme. Cette table ingoings est en relation polymorphique avec quatre tables :
Si vous regardez un peu le contenu de cette table :
Vous voyez que la première colonne (ingoing_id) mémorise les identifiants, et la seconde (ingoing_type) mémorise la classe du modèle correspondant.
VII-B-2. Les modèles▲
Dans le modèle App\Models\Ingoing vous trouvez cette méthode :
Dans les modèles App\Models\Post, App\Models\User, App\Models\Contact, App\Models\Comment, vous trouvez le trait App\Models\IngoingTrait avec ce code :
Et ça suffit pour avoir la relation fonctionnelle !
VII-B-3. La création des ingoings▲
Quand un modèle est créé il y a déclenchement d'un événement, nous verrons cet aspect événementiel dans un chapitre ultérieur. Pour le moment on va juste regarder l'écouteur (listener) de cet événement App\Listeners\ModelCreated.php :
On connaît le modèle créé grâce à l'événement ($event
->
model), on utilise la méthode save sur la relation en passant une instance de Ingoing. Un nouvel enregistrement est ainsi créé dans la table ingoings avec les colonnes ingoing_id et ingoing_type parfaitement renseignées.
VII-B-4. Les panneaux de l'administration▲
Pour gérer les panneaux de l'administration il existe une classe particulière PannelAdmin :
Avec ce code :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
<?php
namespace
App\Services;
class
PannelAdmin
{
...
public
function
__construct
(array
$infos
)
{
$this
->
color =
$infos
[
'color'
];
$this
->
icon =
$infos
[
'icon'
];
$this
->
model =
new
$infos
[
'model'
];
$this
->
name =
__($infos
[
'name'
]
);
$this
->
url =
$infos
[
'url'
];
$this
->
nbr =
$this
->
getNumber ();
}
protected
function
getNumber()
{
return
$this
->
model->
has('ingoing'
)->
count();
}
}
Une classe toute simple qui comporte les 6 propriétés nécessaires pour les panneaux :
- couleur (color) ;
- icône (icon) ;
- modèle (model) ;
- nom (name) ;
- URL (url) ;
- nombre de nouveaux éléments (nbr).
On voit que la dernière propriété est renseignée avec la fonction getNumber, dans laquelle on trouve ce code :
Donc on considère tous les enregistrements qui ont (has) un ingoing et on les compte (count).
Si vous regardez les requêtes générées avec la barre de débogage vous allez trouver ce genre de requête :
select
count
(*)
as
aggregate
from
`contacts`
where
exists
(
select
*
from
`ingoings`
where
`contacts`
.`id`
=
`ingoings`
.`ingoing_id`
and
`ingoings`
.`ingoing_type`
=
'App\Models\Contact'
)
Voici le code dans le contrôleur :
Les panneaux sont définis dans le fichier config/pannels.php :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
<?php
return
[
[
'color'
=>
'primary'
,
'icon'
=>
'envelope'
,
'model'
=>
\App\Models\Contact::
class
,
'name'
=>
'admin.new-messages'
,
'url'
=>
'admin/contacts?new=on'
,
],
...
];
Dans le contrôleur on a une boucle pour itérer tous les panneaux :
Et on ne crée le panneau que s'il y a au moins un nouvel enregistrement :
if ($panelAdmin
->
nbr) {
$pannels
[]
=
$panelAdmin
;
}
On envoie ensuite les panneaux à la vue :
Cette vue resources/views/back/index.blade.php est assez légère :
Je ne vais pas entrer dans le détail de la syntaxe parce que je parlerai plus longuement des vues ultérieurement mais la directive @each de Blade permet de faire une itération, on appelle donc la vue partielle …back/partials/pannel.blade.php pour chaque panneau. C'est la vue partielle qui a le code pour l'affichage :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
<
div class
=
"
col-lg-3 col-xs-6
"
>
<!-- small box -->
<
div class
=
"
small-box bg-
{{
$pannel
->
color }}
"
>
<
div class
=
"
inner
"
>
<
h3>
{{
$pannel
->
nbr }}
<
/h3
>
<
p>
{{
$pannel
->
name }}
<
/p
>
<
/div
>
<
div class
=
"
icon
"
>
<
span class
=
"
fa fa-
{{
$pannel
->
icon }}
"
></span
>
<
/div
>
<
a href
=
"
{{
$pannel
->
url }}
"
class
=
"
small-box-footer
"
>
@lang
('More info'
)
<
span class
=
"
fa fa-arrow-circle-right
"
></span
>
<
/a
>
<
/div
>
<
/div
>
VII-B-5. Suppression des ingoings▲
Dans l'administration quand on affiche la liste d'une entité, par exemple les contacts, on a une case à cocher pour signaler si l'entité est nouvelle ou pas :
Si on décoche ça envoie une requête en Ajax pour la mise à jour dans la base. Je ne vais pas entrer dans le détail du JavaScript mais juste montrer la méthode du contrôleur, ici Back/Contact/Controller :
On utilise la méthode delete sur la relation (ingoing) et c'est fait ! Ensuite on renvoie une réponse vide au format JSON puisqu'on est en Ajax.
D'ailleurs si on regarde la méthode du ContactRepository pour afficher les contacts dans l'administration :
On voit qu'on fait un chargement (with) de l'ingoing. D'autre part quand le paramètre new est présent dans l'URL on ajoute à la requête le filtrage des ingoing ($query
->
has ('
ingoing
'
)) parce qu'on ne veut que les nouveaux contacts, ce qui donne ce genre de requête :
select
*
from
`contacts`
where
exists
(
select
*
from
`ingoings`
where
`contacts`
.`id`
=
`ingoings`
.`ingoing_id`
and
`ingoings`
.`ingoing_type`
=
'App\Models\Contact'
)
order
by
`created_at`
desc
limit
3
offset
0
VII-C. En résumé▲
- Lorsque plusieurs tables sont concernées d'un côté d'une relation on doit appliquer le polymorphisme.
- Laravel propose de nombreuses méthodes pour gérer le polymorphisme selon la situation.