VIII. La relation n:n (2/2)▲
Dans ce chapitre on va poursuivre la réalisation du petit blog avec des tags qu'on a commencé au précédent chapitre. On va voir le fonctionnement de l'ensemble ainsi que les vues.
VIII-A. La liste des articles▲
La méthode du repository pour la liste des articles est modifiée et renommée pour ajouter la table tags :
Pour clarifier le code, j'ai créé une fonction protégée qui va nous servir une autre fois :
Vous remarquez qu'on a ajouté la table tags comme paramètre de la méthode with en plus de users. On va en effet avoir besoin des informations des tags pour l'affichage dans la vue.
Il est intéressant de voir les requêtes générées par Eloquent, par exemple pour la première page :
2.
3.
4.
5.
6.
7.
8.
9.
select
count
(*)
as
aggregate
from
`posts`
select
*
from
`posts`
order
by
`created_at`
desc
limit
4
offset
0
select
*
from
`users`
where
`users`
.`id`
in
(
'3'
, '2'
, '4'
, '5'
)
select
`tags`
.*
, `post_tag`
.`post_id`
as
`pivot_post_id`
, `post_tag`
.`tag_id`
as
`pivot_tag_id`
from
`tags`
inner
join
`post_tag`
on
`tags`
.`id`
=
`post_tag`
.`tag_id`
where
`post_tag`
.`post_id`
in
(
'6'
, '4'
, '9'
, '14'
)
select
*
from
`users`
where
`users`
.`id`
=
'1'
limit
1
On voit que :
- on demande le nombre total d'articles pour la pagination ;
- on demande les quatre premières lignes des articles avec l'ordre des dates ;
- on demande les utilisateurs qui correspondent aux articles sélectionnés ;
- on demande les tags concernés par les articles.
On se rend compte là du travail effectué par Eloquent pour nous !
VIII-B. Nouvel article▲
L'enregistrement d'un nouvel article va évidemment être un peu plus délicat à cause de la présence des tags. Dans le repository des articles (PostRepository), on va se contenter d'enregistrer l'article :
C'est dans le repository des tags que le plus gros du travail va se faire :
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.
<?php
public
function
store($post
,
$tags
)
{
$tags
=
explode(','
,
$tags
);
foreach
($tags
as
$tag
) {
$tag
=
trim($tag
);
$tag_url
=
Str::
slug($tag
);
$tag_ref
=
$this
->
tag->
where('tag_url'
,
$tag_url
)->
first();
if
(is_null($tag_ref
))
{
$tag_ref
=
new
$this
->
tag([
'tag'
=>
$tag
,
'tag_url'
=>
$tag_url
]
);
$post
->
tags()->
save($tag_ref
);
}
else
{
$post
->
tags()->
attach($tag_ref
->
id);
}
}
}
Ce code mérite quelques commentaires. Les tags sont envoyés par le formulaire (que nous verrons plus loin) sous la forme de texte avec comme séparateur une virgule. Par exemple :
tag1,tag2,tag3
Dans le contrôleur la première chose est de vérifier qu'il y a des tags saisis :
Si c'est le cas, on appelle la méthode store du repository en transmettant les tags et une référence du modèle créé. Dans le repository on crée un tableau en utilisant le séparateur (virgule) :
$tags
=
explode('
,
'
,
$tags
);
Ensuite on parcourt le tableau :
Par précaution on supprime les espaces éventuels
$tag
=
trim($tag
);
On crée la version pour URL du tag (avec la méthode slug de la classe Str) :
$tag_url =
Str
::
slug
(
$tag);
On regarde si ce tag existe déjà :
$tag_ref
=
$this
->
tag->
where('
tag_url
'
,
$tag_url
)->
first();
On peut aussi utiliser cette syntaxe pour le where :
$tag_ref
=
$this
->
tag->
whereTagUrl($tag_url
)->
first();
Si ce n'est pas le cas, on le crée :
Remarquez comment la méthode save ici permet à la fois de créer le tag et de référencer la table pivot.
Si le tag existe déjà on se contente d'informer la table pivot avec la méthode attach :
$post
->
tags()->
attach($tag_ref
->
id);
VIII-C. Suppression d'un article▲
Quand on va supprimer un article, il faudra aussi supprimer les liens avec les tags :
La méthode detach permet de supprimer les lignes dans la table pivot.
VIII-D. La recherche par tag▲
Il nous reste enfin à voir la recherche par sélection d'un tag :
Vous remarquez que par rapport au code de la méthode getWithUserAndTagsPaginate on a ajouté la méthode whereHas. Cette méthode permet d'ajouter une condition sur une table chargée. Il est intéressant là aussi de voir les requêtes générées par Eloquent :
2.
3.
4.
5.
6.
7.
8.
9.
select
count
(*)
as
aggregate
from
`posts`
where
exists
(
select
*
from
`tags`
inner
join
`post_tag`
on
`tags`
.`id`
=
`post_tag`
.`tag_id`
where
`post_tag`
.`post_id`
=
`posts`
.`id`
and
`tags`
.`tag_url`
=
'omnis'
)
select
*
from
`posts`
where
exists
(
select
*
from
`tags`
inner
join
`post_tag`
on
`tags`
.`id`
=
`post_tag`
.`tag_id`
where
`post_tag`
.`post_id`
=
`posts`
.`id`
and
`tags`
.`tag_url`
=
'omnis'
)
order
by
`created_at`
desc
limit
4
offset
0
select
*
from
`users`
where
`users`
.`id`
in
(
'3'
, '2'
, '4'
)
select
`tags`
.*
, `post_tag`
.`post_id`
as
`pivot_post_id`
, `post_tag`
.`tag_id`
as
`pivot_tag_id`
from
`tags`
inner
join
`post_tag`
on
`tags`
.`id`
=
`post_tag`
.`tag_id`
where
`post_tag`
.`post_id`
in
(
'6'
, '4'
, '8'
, '10'
)
select
*
from
`users`
where
`users`
.`id`
=
'1'
limit
1
Il y en a cinq plutôt chargées :
- on compte les enregistrements pour la pagination (avec une jointure) ;
- on récupère les quatre lignes des articles (avec une jointure) ;
- on récupère les utilisateurs rédacteurs des articles ;
- on récupère les tags concernés par les articles (avec une jointure).
Il y a une chose que je n'ai pas gérée dans tout ce code, c'est le cas des tags orphelins en cas de suppression d'un article. Cette gestion n'est pas obligatoire parce qu'il n'est pas vraiment gênant d'avoir des tags orphelins. On pourrait prévoir une maintenance épisodique de la base (tâche CRON par exemple) ou une action de l'administrateur.
VIII-E. Le template▲
On conserve le même template (resources/views/layouts/app.blade.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.
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.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
<!
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
"
>
<!-- CSRF Token -->
<
meta name
=
"
csrf-token
"
content
=
"
{{
csrf_token() }}
"
>
<
title>
Mon joli blog<
/title
>
<!-- Styles -->
<
link href
=
"
/css/app.css
"
rel
=
"
stylesheet
"
>
<!-- Scripts -->
<script>
window
.
Laravel =
<?php
echo json_encode([
'csrfToken'
=>
csrf_token(),
]
);
?>
</
script>
<
/head
>
<
body>
<
nav class
=
"
navbar navbar-default navbar-static-top
"
>
<
div class
=
"
container
"
>
<
div class
=
"
navbar-header
"
>
<!-- Collapsed Hamburger -->
<
button type
=
"
button
"
class
=
"
navbar-toggle collapsed
"
data-toggle
=
"
collapse
"
data-target
=
"
#app-navbar-collapse
"
>
<
span class
=
"
sr-only
"
>
Toggle Navigation<
/span
>
<
span class
=
"
icon-bar
"
></span
>
<
span class
=
"
icon-bar
"
></span
>
<
span class
=
"
icon-bar
"
></span
>
<
/button
>
<!-- Branding Image -->
<
a class
=
"
navbar-brand
"
href
=
"
{{
URL('/'
) }}
"
>
Mon joli Blog
<
/a
>
<
/div
>
<
div class
=
"
collapse navbar-collapse
"
id
=
"
app-navbar-collapse
"
>
<!-- Left Side Of Navbar -->
<
ul class
=
"
nav navbar-nav
"
>
<
/ul
>
<!-- Right Side Of Navbar -->
<
ul class
=
"
nav navbar-nav navbar-right
"
>
<!-- Authentication Links -->
@if
(Auth
::
guest
())
<
li><a href
=
"
{{
URL('/login'
) }}
"
>
Se connecter<
/a
></li
>
<
li><a href
=
"
{{
URL('/register'
) }}
"
>
S'enregistrer<
/a
></li
>
@else
<
li><a href
=
"
{{
URL('/post/create'
) }}
"
>
Créer un article<
/a
></li
>
<
li class
=
"
dropdown
"
>
<
a href
=
"
#
"
class
=
"
dropdown-toggle
"
data-toggle
=
"
dropdown
"
role
=
"
button
"
aria-expanded
=
"
false
"
>
{{
Auth::
user()->
name }}
<
span class
=
"
caret
"
></span
>
<
/a
>
<
ul class
=
"
dropdown-menu
"
role
=
"
menu
"
>
<
li>
<
a href
=
"
{{
url('/logout'
) }}
"
onclick
=
"
event.preventDefault();
document.getElementById('logout-form').submit();
"
>
Logout
<
/a
>
<
form id
=
"
logout-form
"
action
=
"
{{
url('/logout'
) }}
"
method
=
"
POST
"
style
=
"
display: none;
"
>
{{
csrf_field() }}
<
/form
>
<
/li
>
<
/ul
>
<
/li
>
@endif
<
/ul
>
<
/div
>
<
/div
>
<
/nav
>
@yield
('content'
)
<!-- Scripts -->
<script src
=
"
/js/app.js
"
></
script>
<
/body
>
<
/html
>
VIII-F. La liste des articles▲
Voici la vue pour la liste des articles (resources/views/posts/liste.blade.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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
@extends
('layouts.app'
)
@section
('content'
)
<
div class
=
"
container
"
>
@if
(isset($info
))
<
div class
=
"
row alert alert-info
"
>
{{
$info
}}
<
/div
>
@endif
{!!
$posts
->
links() !!}
@foreach
($posts
as
$post
)
<
article class
=
"
row bg-primary
"
>
<
div class
=
"
col-md-12
"
>
<
header>
<
h1>
{{
$post
->
titre }}
<
div class
=
"
pull-right
"
>
@foreach
($post
->
tags as
$tag
)
<
a href
=
"
{{
URL('post/tag/'
.
$tag
->
tag_url) }}
"
class
=
"
btn btn-xs btn-info
"
>
{{
$tag
->
tag }}
<
/a
></li
>
@endforeach
<
/div
>
<
/h1
>
<
/header
>
<
hr>
<
section>
<
p>
{{
$post
->
contenu }}
<
/p
>
@if
(auth
()->
check() and
auth
()->
user()->
admin
)
<
form method
=
"
POST
"
action
=
"
{{
route('post.destroy'
,
[
'id'
=>
$post
->
id]
) }}
"
>
{{
method_field('DELETE'
) }}
{{
csrf_field() }}
<
input class
=
"
btn btn-danger btn-xs
"
onclick
=
"
return confirm('Vraiment supprimer cet article ?')
"
type
=
"
submit
"
value
=
"
Supprimer cet article
"
>
<
/form
>
@endif
<
em class
=
"
pull-right
"
>
{{
$post
->
user->
name }}
le {!!
$post
->
created_at->
format('d-m-Y'
) !!}
<
/em
>
<
/section
>
<
/div
>
<
/article
>
<
br>
@endforeach
{!!
$posts
->
links() !!}
<
/div
>
@endsection
Avec cet aspect :
Les tags apparaissent sous la forme de petits boutons. Le fait de cliquer sur un de ces boutons lance la recherche à partir de ce tag et affiche les articles correspondants ainsi qu'une barre d'information :
Un utilisateur connecté dispose en plus du lien pour créer un article. L'administrateur a en plus le bouton de suppression :
VIII-G. La création d'un article▲
Le formulaire de création d'un article (resources/views/posts/add.blade.php) a été enrichi d'un contrôle de texte pour la saisie des tags :
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.
@extends
('layouts.app'
)
@section
('content'
)
<
div class
=
"
col-sm-offset-3 col-sm-6
"
>
<
div class
=
"
panel panel-default
"
>
<
div class
=
"
panel-heading
"
>
Ajout d'un article<
/div
>
<
div class
=
"
panel-body
"
>
<
form method
=
"
POST
"
action
=
"
{{
URL('/post'
) }}
"
>
{{
csrf_field() }}
<
div class
=
"
form-group
{{
$errors
->
has('titre'
) ?
' has-error'
:
''
}}
"
>
<
input class
=
"
form-control
"
placeholder
=
"
Titre
"
name
=
"
titre
"
type
=
"
text
"
value
=
"
{{
old('titre'
) }}
"
autofocus>
@if
($errors
->
has('titre'
))
<
span class
=
"
help-block
"
>
<
strong>
{{
$errors
->
first('titre'
) }}
<
/strong
>
<
/span
>
@endif
<
/div
>
<
div class
=
"
form-group
{{
$errors
->
has('contenu'
) ?
' has-error'
:
''
}}
"
>
<
textarea class
=
"
form-control
"
placeholder
=
"
Contenu
"
name
=
"
contenu
"
cols
=
"
50
"
rows
=
"
10
"
>
{{
old('contenu'
) }}
<
/textarea
>
@if
($errors
->
has('contenu'
))
<
span class
=
"
help-block
"
>
<
strong>
{{
$errors
->
first('contenu'
) }}
<
/strong
>
<
/span
>
@endif
<
/div
>
<
div class
=
"
form-group
{{
$errors
->
has('tags'
) ?
' has-error'
:
''
}}
"
>
<
input class
=
"
form-control
"
placeholder
=
"
Entrez les tags séparés par des virgules
"
name
=
"
tags
"
type
=
"
text
"
value
=
"
{{
old('tags'
) }}
"
>
@if
($errors
->
has('tags'
))
<
span class
=
"
help-block
"
>
<
strong>
{{
$errors
->
first('tags'
) }}
<
/strong
>
<
/span
>
@endif
<
/div
>
<
button type
=
"
submit
"
class
=
"
btn btn-primary pull-right
"
>
Envoyer !<
/button
>
<
/form
>
<
/div
>
<
/div
>
<
/div
>
@endsection
Est aussi géré le message d'erreur pour la validation des tags :
Je ne détaille pas le code de toutes ces vues, il n'est pas bien compliqué et recouvre des situations déjà rencontrées.
VIII-H. En résumé▲
- Eloquent permet de générer de nombreuses requêtes SQL à partir de simples méthodes explicites.
- On n'a pas besoin de modèle pour la table pivot qui est prise en charge complètement par les modèles encadrants.