ch5 - Relation 1 - N
ch5 - Relation 1 - N
ch5 - Relation 1 - N
1:n
www.bibliovox.com:Fondation Mohammed VI:2110057491:88834657:105.155.23.201:1587499977
Pour le moment, nous n’avons manipulé qu’une table avec Eloquent. Dans le pré-
sent chapitre, nous allons en utiliser deux et les mettre en relation. La relation la plus
répandue et la plus simple est celle qui fait correspondre un enregistrement d’une table
à plusieurs enregistrements de l’autre table ; on parle de relation de un à plusieurs ou
encore de relation de type 1:n. Nous verrons également dans ce chapitre comment
créer un middleware.
Les données
Migrations
Continuons à utiliser la table users des chapitres précédents. Nous allons ajouter une
nouvelle table posts destinée à mémoriser les articles. Si vous avez déjà créé la table
users avec des enregistrements, supprimez-la ; nous allons la recréer.
Nous avons déjà défini la migration de la table users et vous devez avoir le fichier dans
le dossier app/database/migrations. Je vous en rappelle le code :
Deuxième partie – Les bases de données
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
162
Chapitre 14. La relation 1:n
Lancez la migration :
Vous devez ainsi vous retrouver avec les trois tables dans votre base, ainsi que la table
migrations.
Pour que la population que nous allons créer ensuite fonctionne, il faut repartir sur une
! nouvelle migration pour la table users. En effet, on va avoir besoin que la clé de la
table commence à 1.
163
Deuxième partie – Les bases de données
Population
Nous allons remplir nos tables avec des enregistrements pour réaliser nos essais. Créons
pour cela deux fichiers dans le dossier database/seeds. Normalement, vous devez
déjà avoir dans ce dossier le fichier DatabaseSeeder.php.
En voici le code :
<?php
use Illuminate\Database\Seeder;
La méthode run est destinée à exécuter les fichiers pour la population. Vous avez déjà
la ligne commentée de lancement pour la table users. Nous allons la dé-commenter
et ajouter le code pour la table posts :
<?php
public function run()
{
$this->call(UserTableSeeder::class);
$this->call(PostTableSeeder::class);
}
Placez bien les lignes dans cet ordre ; vous comprendrez bientôt pourquoi c’est
nécessaire.
Ensuite, on va créer le fichier UserTableSeeder.php pour la population de la table
users :
164
Chapitre 14. La relation 1:n
<?php
use Illuminate\Database\Seeder;
<?php
use Illuminate\Database\Seeder;
use Carbon\Carbon;
165
Deuxième partie – Les bases de données
Cela générera cent articles affectés de façon aléatoire aux dix utilisateurs. Nous étu-
dierons bientôt comment s’effectue la liaison entre les deux.
Normalement, vous devez obtenir les deux tables remplies à l’issue de cette commande.
La table users
166
Chapitre 14. La relation 1:n
La relation
On a la situation suivante :
• un utilisateur peut écrire plusieurs articles ;
• un article est écrit par un seul utilisateur.
La clé étrangère
Vous voyez la relation dessinée entre la clé id dans la table users et la clé étrangère
user_id dans la table posts. La migration qu’on a créée informe la base de cette
relation, via le code suivant :
<?php
$table->foreign('user_id')
->references('id')
->on('users')
->onDelete('restrict')
->onUpdate('restrict');
Dans la table, on déclare une clé étrangère (foreign) nommée user_id qui réfé-
rence (references) la ligne id dans la table (on) users. En cas de suppression
(onDelete) ou de modification (onUpdate), on a une restriction (restrict). Que
signifient ces deux dernières conditions ?
Imaginez que vous ayez un utilisateur avec l’id 5 associé à deux articles. Dans la
table posts, deux enregistrements ont donc un user_id de valeur 5. Si on sup-
prime l’utilisateur, la clé étrangère de ces deux enregistrements ne correspond plus à
aucun enregistrement dans la table users. En indiquant restrict, on empêche la
suppression d’un utilisateur auquel est associé au moins un article. Il faut commencer
par supprimer ses articles avant de le supprimer lui-même. On dit que la base assure
167
Deuxième partie – Les bases de données
l’intégrité référentielle. Elle n’accepte pas non plus qu’on utilise pour user_id une
valeur qui n’existe pas dans la table users.
Une autre possibilité est cascade à la place de restrict. Dans ce cas, si vous sup-
primez un utilisateur, tous les articles associés sont également effacés. C’est une option
qui est rarement utilisée parce qu’elle peut s’avérer dangereuse, surtout dans une base
Les modèles
Nous avons déjà un modèle User (app/User.php). Il faudra juste lui ajouter une
méthode pour trouver facilement les articles d’un utilisateur :
<?php
public function posts()
{
return $this->hasMany('App\Post');
}
On déclare ici qu’un utilisateur a plusieurs (hasMany) articles (posts). On aura ainsi
une méthode pratique pour récupérer les articles d’un utilisateur.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
168
Chapitre 14. La relation 1:n
Ici, la méthode user (au singulier) trouve l’utilisateur auquel appartient (belongsTo)
l’article. C’est donc la réciproque de la méthode précédente.
Voici une schématisation de cette relation avec les deux méthodes.
Je vous rappelle que si vous ne spécifiez pas de manière explicite le nom de la table
dans un modèle, Laravel le déduit à partir du nom du modèle qu’il met au pluriel (à la
mode anglaise) et avec la première lettre en minuscule.
Les deux méthodes mises en place récupèrent facilement un enregistrement lié. Par
exemple, cherchons tous les articles de l’utilisateur qui a l’id 1 :
<?php
$articles=App\User::find(1)->posts;
De la même manière, on peut trouver l’utilisateur qui a écrit l’article d’id 1 :
<?php
$user=App\Post::find(1)->user;
Laravel dispose de l’outil tinker qui permet d’entrer des commandes dans la console
et ainsi d’interagir directement avec l’application. Il faut le démarrer avec la commande
php artisan tinker. On peut ensuite l’utiliser directement :
169
Deuxième partie – Les bases de données
Contrôleur et routes
Contrôleur
Tout est en place au niveau des données ; voyons donc un peu comment gérer tout
cela. Créons le contrôleur pour les articles, PostController. Il doit gérer plusieurs
choses :
• recevoir la requête pour afficher les articles du blog et fournir la réponse adaptée ;
• recevoir la requête pour le formulaire destiné à la création d’un nouvel article et y
répondre ;
• recevoir le formulaire soumis (réservé à un utilisateur connecté) et l’enregistrer ;
• recevoir la demande de suppression d’un article (réservé à un administrateur) et
supprimer l’enregistrement correspondant.
Pour simplifier, je ne vais pas prévoir la possibilité de modifier un article.
J’utiliserai un contrôleur de ressource. Voici son code :
<?php
namespace App\Http\Controllers;
use App\Repositories\PostRepository;
use App\Http\Requests\PostRequest;
170
Chapitre 14. La relation 1:n
$this->postRepository=$postRepository;
}
Routes
On a vu dans le chapitre sur les ressources comment créer les routes de ce genre de
contrôleur. Il faut juste indiquer qu’on ne veut pas utiliser les sept méthodes dispo-
nibles, mais seulement certaines :
<?php
Route::resource('post', 'PostController', ['except'=>['show', 'edit',
'update']]);
...
Avec except, j’indique que je ne veux pas de route pour les trois méthodes citées.
Voici ce que cela donne en utilisant Artisan pour visualiser les routes (php arti-
san route:list).
171
Deuxième partie – Les bases de données
Les routes
<?php
namespace App\Repositories;
use App\Post;
class PostRepository
{
protected $post;
$this->post->findOrFail($id)->delete();
}
}
172
Chapitre 14. La relation 1:n
Les middlewares
<?php
namespace App\Http\Middleware;
use Closure;
class Admin
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->user()->admin)
{
return $next($request);
}
return new RedirectResponse(url('post/liste'));
}
}
173
Deuxième partie – Les bases de données
l’inverse, on tomberait évidemment sur une erreur en cas de tentative d’accès à l’URL
pour la suppression d’un article.
On a créé le middleware, mais cela ne suffit pas ; il faut maintenant un lien entre le
nom qu’on veut donner au filtre et la classe qu’on vient de créer. Regardez dans le
fichier app/Http/Kernel.php qui contient tous les middlewares déclarés et ajoutez
la ligne de code correspondant à admin :
La validation
La requête de formulaire
On complète le code :
<?php
namespace App\Http\Requests;
use App\Http\Requests\Request;
174
Chapitre 14. La relation 1:n
return [
'titre'=>'required|max:80',
'contenu'=>'required'
];
}
}
La liste des articles est obtenue avec l’URL .../post (verbe get) qui arrive sur la méthode
index du contrôleur :
<?php
public function index()
{
$posts=$this->postRepository->getPaginate($this->nbrPerPage);
$links=$posts->render();
return view('posts.liste', compact('posts', 'links'));
}
Ici, on envoie le nombre d’articles par page (placé dans la propriété $nbrPerPage) à
la méthode getPaginate du gestionnaire :
<?php
public function getPaginate($n)
{
return $this->post->with('user')
->orderBy('posts.created_at', 'desc')
->paginate($n);
}
On veut les articles avec (with) l’utilisateur (user), dans l’ordre des dates de création
(posts.created_at) descendant (desc) avec une pagination de n articles ($n).
Il existe la méthode latest (et oldest pour l’inverse) qui simplifie la syntaxe :
<?php
return $this->post->with('user')
->latest('posts.created_at')
->paginate($n);
175
Deuxième partie – Les bases de données
<?php
<?php
public function store(PostRequest $request)
{
$inputs=array_merge($request->all(), ['user_id'=>$request->user()->id]);
$this->postRepository->store($inputs);
return redirect(route('post.index'));
}
<?php
public function store($inputs)
{
$this->post->create($inputs);
}
<?php
public function destroy($id)
{
$this->postRepository->destroy($id);
return redirect()->back();
}
176
Chapitre 14. La relation 1:n
<?php
public function destroy($id)
{
$this->post->findOrFail($id)->delete();
}
Les vues
Template
<!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>Mon joli site</title>
{!! Html::style('https://netdna.bootstrapcdn.com/bootstrap/3.3.6/css/
bootstrap.min.css') !!}
{!! Html::style('https://netdna.bootstrapcdn.com/bootstrap/3.3.6/css/
bootstrap-theme.min.css') !!}
<!--[if lt IE 9]>
{{ Html::style('https://oss.maxcdn.com/libs/html5shiv/3.7.2/html5shiv.
js') }}
{{ Html::style('https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.
min.js') }}
<![endif]-->
<style>textarea{resize:none;}</style>
</head>
<body>
<header class="jumbotron">
<div class="container">
<h1 class="page-header">{!! link_to_route('post.index', 'Mon joli
blog') !!}</h1>
@yield('header')
</div>
</header>
<div class="container">
@yield('contenu')
</div>
</body>
</html>
177
Deuxième partie – Les bases de données
Nous avons besoin d’une vue pour afficher les articles du blog et quelques boutons pour
la gestion (resources/views/posts/liste.blade.php) :
@extends('template')
@section('contenu')
@if(isset($info))
<div class="row alert alert-info">{{$info}}</div>
@endif
{!!$links!!}
@foreach($posts as $post)
<article class="row bg-primary">
<div class="col-md-12">
<header>
<h1>{{$post->titre}}</h1>
</header>
<hr>
<section>
<p>{{$post->contenu}}</p>
@if(Auth::check() and Auth::user()->admin)
{!! Form::open(['method'=>'DELETE', 'route'=>['post.destroy',
$post->id]]) !!}
{!! Form::submit('Supprimer cet article', ['class'=>'btn
btn-danger btn-xs', 'onclick'=>'return confirm(\'Vraiment supprimer cet
article ?\')']) !!}
{!! Form::close() !!}
@endif
<em class="pull-right">
<span class="glyphicon glyphicon-pencil"></span> {{$ post->user-
>name }} le {!! $post->created_at->format('d-m-Y') !!}
</em>
</section>
</div>
</article>
<br>
@endforeach
{!!$links!!}
@endsection
178
Chapitre 14. La relation 1:n
L’aspect du blog
Le bouton Se connecter envoie sur l’URL .../login, ce qui doit aboutir sur le formulaire
de connexion si vous avez tout en place comme nous l’avons prévu précédemment.
Le formulaire de connexion
Pour mémoire, j’utilise ici les vues par défaut de Laravel. Vous êtes évidemment libres
de franciser ces vues ou, encore mieux, les adapter au langage de l’utilisateur (nous
verrons cet aspect dans un chapitre ultérieur).
Pour que votre application fonctionne bien avec le AuthController, il faut modifier
la propriété redirectTo dans ce contrôleur :
<?php
protected $redirectTo='post';
179
Deuxième partie – Les bases de données
De la même manière, il faut prévoir une redirection après déconnexion dans le même
contrôleur :
<?php
protected $redirectAfterLogout='post';
@foreach($posts as $post)
...
@endforeach
Si vous vous connectez avec un utilisateur qui n’est pas administrateur (regardez dans votre
table pour en trouver un car, comme la population est aléatoire, on ne sait pas à l’avance
qui l’est et qui ne l’est pas). Vous retournez au blog avec deux boutons supplémentaires.
@if(Auth::check())
...
@else
...
@endif
Le premier bouton, Créer un article, génère l’URL .../post/create, ce qui a pour effet
d’obtenir le formulaire de création.
Le second bouton, Déconnexion, génère l’URL .../logout, qui correspond aussi à ce que
nous avons vu dans le chapitre précédent.
Si l’utilisateur connecté est un administrateur, alors il a en plus pour chaque article un
bouton de suppression.
180
Chapitre 14. La relation 1:n
@extends('template')
@section('contenu')
<br>
<div class="col-sm-offset-3 col-sm-6">
<div class="panel panel-info">
<div class="panel-heading">Ajout d'un article</div>
<div class="panel-body">
{!! Form::open(['route'=>'post.store']) !!}
<div class="form-group {!! $errors->has('titre') ? 'has-
error' : '' !!}">
{!! Form::text('titre', null, ['class'=>'form-control',
'placeholder'=>'Titre']) !!}
{!! $errors->first('titre', '<small class="help-block">:message</
small>') !!}
</div>
<div class="form-group {!! $errors->has('contenu') ? 'has-
error' : '' !!}">
{!! Form::textarea ('contenu', null, ['class'=>'form-control',
'placeholder'=>'Contenu']) !!}
181
Deuxième partie – Les bases de données
Il n’y a rien de bien nouveau dans cette vue. Voici son apparence.
On peut entrer le titre et le contenu, qui seront ensuite validés dans le contrôleur et
enregistrés si tout se passe bien.
Il arrive parfois que les vues ne se génèrent pas correctement. Laravel utilise un cache
pour les vues en storage/framework/views. Vous pouvez sans problème
! supprimer tous les fichiers (mais pas .gitignore !) pour obliger Laravel à générer
de nouvelles vues. Il existe une commande Artisan pour le faire : php artisan
view:clear.
En résumé
• Une relation de type 1:n nécessite la création d’une clé étrangère côté n.
• On peut remplir les tables d’enregistrements avec la population.
• Une relation dans la base nécessite la mise en place de méthodes spéciales dans les
modèles.
• Avec les middlewares, il est facile de gérer l’accès aux méthodes des contrôleurs.
182