IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Tutoriel pour apprendre à utiliser le framework Laravel 5.3

Les données


précédentsommairesuivant

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 :

 
Sélectionnez
1.
2.
3.
4.
public function getWithUserAndTagsPaginate($n)
{
    return $this->queryWithUserAndTags()->paginate($n);
}

Pour clarifier le code, j'ai créé une fonction protégée qui va nous servir une autre fois :

 
Sélectionnez
1.
2.
3.
4.
protected function queryWithUserAndTags()
{
    return $this->post->with('user', 'tags')->latest();
}

‌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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
2.
3.
4.
public function store($inputs)
{
    return $this->post->create($inputs);
}

C'est dans le repository des tags que le plus gros du travail va se faire :

 
Sélectionnez
1.
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 :

 
Sélectionnez
tag1,tag2,tag3

Dans le contrôleur la première chose est de vérifier qu'il y a des tags saisis :

 
Sélectionnez
1.
2.
3.
4.
if(isset($inputs['tags'])) 
{
    $tagRepository->store($post, $inputs['tags']);
}

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) :

 
Sélectionnez
$tags = explode(',', $tags);

Ensuite on parcourt le tableau :

 
Sélectionnez
foreach ($tags as $tag)

Par précaution on supprime les espaces éventuels

 
Sélectionnez
$tag = trim($tag);

On crée la version pour URL du tag (avec la méthode slug de la classe Str) :

 
Sélectionnez
$tag_url = Str::slug($tag);

On regarde si ce tag existe déjà :

 
Sélectionnez
$tag_ref = $this->tag->where('tag_url', $tag_url)->first();

On peut aussi utiliser cette syntaxe pour le where :

 
Sélectionnez
$tag_ref = $this->tag->whereTagUrl($tag_url)->first();

Si ce n'est pas le cas, on le crée :

 
Sélectionnez
1.
2.
3.
4.
5.
$tag_ref = new $this->tag([
    'tag' => $tag,
    'tag_url' => $tag_url
]); 
$post->tags()->save($tag_ref);

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 :

 
Sélectionnez
$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 :

 
Sélectionnez
1.
2.
3.
4.
5.
public function destroy(Post $post)
{
    $post->tags()->detach();
    $post->delete();
}

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
public function getWithUserAndTagsForTagPaginate($tag, $n)
{
    return $this->queryWithUserAndTags()
        ->whereHas('tags', function($query) use ($tag) {
            $query->where('tags.tag_url', $tag);
        })->paginate($n);
}

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 :

 
Sélectionnez
1.
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) :

 
Sélectionnez
1.
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) :

 
Sélectionnez
1.
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 :

Image non disponible

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 :

Image non disponible

Un utilisateur connecté dispose en plus du lien pour créer un article. L'administrateur a en plus le bouton de suppression :

Image non disponible

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 :

 
Sélectionnez
1.
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 :

Image non disponible

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.

précédentsommairesuivant

Copyright © 2017 Laravel. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.