Tutoriel pour apprendre à utiliser le framework Laravel 5.3

Les données

bestmomo (Laravel)

Dans ce tutoriel, nous allons commencer à aborder les bases de données. C'est un vaste sujet auquel Laravel apporte des réponses efficaces. Nous allons commencer par voir les migrations et les modèles.

Pour ce tutoriel je vais encore prendre un exemple simple en imaginant un formulaire destiné à l'inscription à une lettre d'information. On va se contenter d'envoyer un email et mémoriser cet email dans une table d'une base de données.

Commentez

- Titre : Tutoriel pour apprendre à utiliser le framework Laravel 5.3

- Auteur : bestmomo (Laravel)

- Parution : 10 mai 2017

- Licence : Copyright ® 2017 Laravel. Aucune reproduction, même partielle, ne peut être faite de ce site et 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.

Sommaire

- I. Migrations et modèles

- I-A. Les migrations

- I-A-1. La configuration de la base

- I-A-2. Artisan

- I-A-3. Créer la migration

- I-A-4. Utiliser la migration

- I-B. Eloquent et le modèle

- I-C. La validation

- I-D. Les routes et le contrôleur

- I-D-1. Routes

- I-D-2. Contrôleur

- I-E. Les vues

- I-F. Le fonctionnement

- I-G. Organisation du code

- I-G-1. Première version

- I-G-2. Seconde version

- I-G-3. Troisième version

- I-H. En résumé

- II. Les ressources (1/2)

- II-A. Les données

- II-A-1. La migration

- II-B. Une ressource

- II-B-1. Création

- II-C. Les routes

- II-D. Le contrôleur

- II-D-1. Liaison implicite

- II-E. La validation

- II-E-1. Création d'un utilisateur

- II-E-2. Modification d'un utilisateur

- II-F. En résumé

- III. Les ressources (2/2) et les erreurs

- III-A. Le gestionnaire de données (repository)

- III-B. Le fonctionnement

- III-B-1. index

- III-B-2. show

- III-B-3. create

- III-B-4. store

- III-B-5. edit

- III-B-6. update

- III-B-7. destroy

- III-C. Le template

- III-D. La vue index

- III-D-1. Structures de contrôle de Blade

- III-D-2. Helper link_to_route

- III-D-3. Formulaire pour la suppression

- III-E. La vue show

- III-F. La vue edit

- III-G. La vue create

- III-H. Les erreurs

- III-I. En résumé

- IV. L'authentification

- IV-A. La base de données

- IV-A-1. La table users

- IV-A-2. La table password_reset

- IV-B. Les middlewares

- IV-B-1. Middleware auth

- IV-B-2. Middleware guest

- IV-C. Les routes de l'authentification

- IV-D. Les vues de l'authentification

- IV-E. L'enregistrement d'un utilisateur

- IV-E-1. La vue register

- IV-E-2. La validation

- IV-E-3. La création de l'utilisateur

- IV-F. La connexion d'un utilisateur

- IV-F-1. La vue login

- IV-F-2. La validation

- IV-F-3. Se rappeler de moi

- IV-F-4. La redirection

- IV-G. La déconnexion d'un utilisateur

- IV-H. En résumé

- V. L'oubli du mot de passe

- V-A. La base de données

- V-B. Les routes et les contrôleurs

- V-C. Première étape : la demande

- V-D. Deuxième étape : le renouvellement

- V-E. En résumé

- VI. La relation 1:n

- VI-A. Les migrations

- VI-A-1. Table users

- VI-A-2. Table posts

- VI-B. La relation

- VI-C. Les modèles

- VI-D. Le contrôleur et les routes

- VI-D-1. Le contrôleur

- VI-D-2. Les routes

- VI-E. Le repository

- VI-F. Les middlewares

- VI-G. La validation

- VI-H. La population

- VI-H-1. Les fabriques (model factories)

- VI-H-2. La population

- VI-I. Fonctionnement

- VI-I-1. La liste des articles

- VI-I-2. L'ajout d'un article

- VI-I-3. Suppression d'un article

- VI-J. Le template

- VI-K. L'affichage des articles

- VI-L. La création d'un article

- VI-M. En résumé

- VII. La relation n:n (1/2)

- VII-A. Les données

- VII-A-1. La relation n:n

- VII-A-2. Les migrations

- VII-B. Les modèles

- VII-C. La population

- VII-D. La validation

- VII-E. Le contrôleur et les routes

- VII-E-1. Le contrôleur

- VII-E-2. Les routes

- VII-F. Les repositories

- VII-F-1. Le repository pour les articles

- VII-F-2. Le repositoy pour les tags

- VII-G. En résumé

- VIII. La relation n:n (2/2)

- VIII-A. La liste des articles

- VIII-B. Nouvel article

- VIII-C. Suppression d'un article

- VIII-D. La recherche par tag

- VIII-E. Le template

- VIII-F. La liste des articles

- VIII-G. La création d'un article

- VIII-H. En résumé

- IX. Un concepteur de schéma

- IX-A. Mise en route

- IX-B. Créer des tables

- IX-B-1. Table des auteurs

- IX-B-2. Table des éditeurs

- IX-B-3. Table des livres

- IX-B-4. Table pivot

- IX-C. Créer les relations

- IX-C-1. Les éditeurs

- IX-C-2. Les livres

- IX-C-3. Les auteurs

- IX-D. La génération

- IX-E. En résumé

- X. Manipuler les données

- X-A. Les données

- X-B. Tinker

- X-C. Les sélections simples

- X-C-1. Liste

- X-C-2. Enregistrement particulier

- X-C-3. Lignes distinctes

- X-C-4. Plusieurs conditions

- X-C-5. Encadrer des valeurs

- X-C-6. Prendre des valeurs dans un tableau

- X-C-7. Agrégations

- X-C-8. Les erreurs

- X-D. Les sélections avec plusieurs tables

- X-D-1. Trouver les titres des livres pour un éditeur dont on a l'id

- X-D-2. Trouver les livres d'un auteur dont on connaît le nom

- X-E. Attention aux requêtes imbriquées !

- X-F. Insérer des enregistrements

- X-F-1. Méthode « save »

- X-F-2. Méthode « create »

- X-F-3. Création si un enregistrement n'existe pas

- X-G. Agir sur la table pivot

- X-G-1. Attacher

- X-G-2. Détacher

- X-G-3. Synchroniser

- X-H. Mettre à jour des enregistrements

- X-H-1. Méthode « save »

- X-H-2. Méthode « update »

- X-I. Supprimer des enregistrements

- X-I-1. Méthode « delete »

- X-J. En résumé

- XI. Remerciements

I - Migrations et modèles

À partir de ce chapitre, il serait souhaitable que vous installiez une barre de débogage. La plus utile est celle proposée par barryvdh. Suivez les indications fournies pour l'installation, ça vous fera un bon exercice.

I-A - Les migrations

Une migration permet de créer et de mettre à jour un schéma de base de données. Autrement dit, vous pouvez créer des tables, des colonnes dans ces tables, en supprimer, créer des index… Tout ce qui concerne la maintenance de vos tables peut être pris en charge par cet outil. Vous avez ainsi un suivi de vos modifications.

I-A-1 - La configuration de la base

Vous devez dans un premier temps avoir une base de données. Laravel permet de gérer les bases de type MySQL, Postgres, SQLite et SQL Server. Je ferai tous les exemples avec MySQL, mais le code sera aussi valable pour les autres types de bases.

Il faut indiquer où se trouve votre base, son nom, le nom de l'utilisateur, le mot de passe dans le fichier de configuration .env :

1. DB_CONNECTION=mysql

2. DB_HOST=127.0.0.1

3. DB_PORT=3306

4. DB_DATABASE=homestead

5. DB_USERNAME=homestead

6. DB_PASSWORD=secret

Ici nous avons les valeurs par défaut à l'installation de Laravel.

Voici par exemple mes réglages pour ma base de test MySQL nommée « tuto » avec MySQL en local non sécurisé :

DB_DATABASE=tuto
DB_USERNAME=root
DB_PASSWORD=

I-A-2 - Artisan

Nous avons déjà utilisé Artisan qui permet de faire beaucoup de choses, vous avez un aperçu des commandes en entrant :

php artisan

Vous avez une longue liste. Pour ce chapitre nous allons nous intéresser uniquement à celles qui concernent les migrations :

1. Migrate              Run the database migrations

2. ...

3. migrate

4. migrate:install      Create the migration repository

5. migrate:refresh      Reset and re-run all migrations

6. migrate:reset        Rollback all database migrations

7. migrate:rollback     Rollback the last database migration

8. migrate:status       Show the status of each migration

I-A-3 - Créer la migration

On va créer la migration pour notre table :

php artisan make:migration create_emails_table

Si vous regardez maintenant dans le dossier database/migrations, vous trouvez un fichier du genre 2016_08_31_212841_create_emails_table.php (la partie numérique qui inclut la date sera évidemment différente pour vous) :

Mais il y a déjà des migrations présentes, à quoi servent-elles ?

Il y a déjà effectivement deux migrations présentes :

- table users : c'est une migration de base pour créer une table des utilisateurs ;

- table password_resets : c'est une migration liée à la précédente qui permet de gérer le renouvellement des mots de passe en toute sécurité.

Nous nous intéresserons à ces migrations dans un chapitre ultérieur. Comme nous n'allons pas avoir besoin immédiatement de ces migrations, le mieux est de les supprimer pour le moment pour éviter de créer des tables inutiles :

Voici le contenu de la migration que nous venons de créer :

1. <?php

3. use Illuminate\Support\Facades\Schema;

4. use Illuminate\Database\Schema\Blueprint;

5. use Illuminate\Database\Migrations\Migration;

7. class CreateEmailsTable extends Migration

8. {

9. /**

10. * Run the migrations.

11. *

12. * @return void

13. */

14. public function up()

15. {

16. //

17. }

19. /**

20. * Reverse the migrations.

21. *

22. * @return void

23. */

24. public function down()

25. {

26. //

27. }

28. }

On dispose dans cette classe de deux fonctions :

- up : ici on mettra le code de création ;

- down : ici on mettra le code de suppression.

On veut créer une table « emails » avec :

- un id autoincrémenté ;

- un champ « email » de type texte ;

- des champs pour mémoriser les dates de création et de modification.

Voilà le code correspondant :

1. public function up()

2. {

3. Schema::create('emails', function(Blueprint $table) {

4. $table->increments('id');

5. $table->string('email');

6. $table->timestamps();

7. });

8. }

On demande au constructeur de schéma (Schema) de créer (create) la table « emails ». Dans la fonction anonyme, on définit ce qu'on veut pour la table :

- une colonne « id » auto-incrémentée qui sera ainsi la clé primaire de la table ;

- une colonne « email » de type string ;

- deux colonnes pour les dates (générées par la méthode timestamps).

Pour la méthode down on va juste supprimer la table avec un drop :

1. public function down()

2. {

3. Schema::drop('emails');

4. }

Notre migration est maintenant créée.

I-A-4 - Utiliser la migration

On va maintenant utiliser la migration (méthode up de la migration) :

Si on regarde maintenant dans la base, on trouve la table « emails » avec ces quatre colonnes :

Vous avez aussi la création d'une table migrations :

Cette table sert à l'intendance des migrations et vous ne devez pas y toucher.

Si vous avez fait une erreur, vous pouvez revenir en arrière avec un rollback qui annule la dernière migration effectuée (utilisation de la méthode down de la migration) :

La table a maintenant été supprimée de la base. Comme on va avoir besoin de cette table, on relance la migration.

On peut aussi effectuer un rafraîchissement de toutes les migrations avec la commande refresh (rollback de toutes les migrations et nouveau lancement de toutes les migrations).

Vous disposez également de la commande status pour savoir où vous en êtes :

Mais pour le moment avec une seule migration, il n'y a vraiment pas de quoi se perdre.

I-B - Eloquent et le modèle

Laravel propose un ORM (acronyme de object-relational mapping ou en bon français un mappage objet-relationnel) très performant.

De quoi s'agit-il ?

Tout simplement que tous les éléments de la base de données ont une représentation sous forme d'objets manipulables.

Quel intérêt ?

Tout simplement de simplifier grandement les opérations sur la base comme nous allons le voir dans toute cette partie du cours.

Avec Eloquent une table est représentée par une classe qui étend la classe Model. Pour notre table emails, on va à nouveau utiliser Artisan pour la création du modèle :

php artisan make:model Email

On trouve le fichier ici :

Avec cette trame de base :

1. <?php

3. namespace App;

5. use Illuminate\Database\Eloquent\Model;

7. class Email extends Model

8. {

9. //

10. }

On va se contenter de ce code pour notre exemple.

On peut créer le modèle en même temps que la migration pour la table avec cette syntaxe :

php artisan make:model Email -m

Nous allons voir maintenant comment utiliser cette classe en construisant notre petite application.

I-C - La validation

Pour la validation on va encore créer une requête de formulaire :

php artisan make:request EmailRequest

On trouve la requête dans son dossier :

La voici avec le code complété :

1. <?php

3. namespace App\Http\Requests;

5. use Illuminate\Foundation\Http\FormRequest;

7. class EmailRequest extends FormRequest

8. {

9. /**

10. * Determine if the user is authorized to make this request.

11. *

12. * @return bool

13. */

14. public function authorize()

15. {

16. return true;

17. }

19. /**

20. * Get the validation rules that apply to the request.

21. *

22. * @return array

23. */

24. public function rules()

25. {

26. return ['email' => 'bail|required|email|unique:emails'];

27. }

28. }

On a quatre règles :

- bail : on s'arrête à la première erreur ;

- required : le champ est requis ;

- email : on doit avoir une adresse email valide ;

- unique : l'email ne doit pas déjà exister (unique) dans la table emails (on sous-entend qu'il s'agit de la colonne email).

Remarquez la puissance de la dernière règle : Eloquent va vérifier que notre email n'existe pas déjà dans la table !

I-D - Les routes et le contrôleur

I-D-1 - Routes

On va avoir deux routes :

Route::get('email', 'EmailController@create');
Route::post('email', 'EmailController@store')->name('store.email');

Remarquez que la seconde route est nommée (store.email).

L'URL de base sera : http://monsite.fr/email

I-D-2 - Contrôleur

On crée le contrôleur avec Artisan :

php artisan make:controller EmailController

On le trouve dans son dossier :

Le code du contrôleur va reprendre l'essentiel de ce que nous avons vu dans les chapitres précédents en utilisant à nouveau la validation injectée. Modifiez ainsi le code :

1. <?php

3. namespace App\Http\Controllers;

5. use App\Email;

6. use App\Http\Requests\EmailRequest;

8. class EmailController extends Controller

9. {

10. public function create()

11. {

12. return view('email');

13. }

15. public function store(EmailRequest $request)

16. {

17. $email = new Email;

18. $email->email = $request->email;

19. $email->save();

21. return view('email_ok');

22. }

23. }

La nouveauté réside uniquement dans l'utilisation du modèle :

$email = new Email;
$email->email = $request->email;
$email->save();

Ici on crée une nouvelle instance de Email. On affecte l'attribut email avec la valeur de l'entrée. Enfin on demande au modèle d'enregistrer cette ligne effectivement dans la table (save).

On ne se soucie pas des colonnes des dates (created_at et updated_at) qui sont automatiquement renseignées par Eloquent.

I-E - Les vues

On va utiliser le même tempate que dans les précédents chapitres (resources/views/template.blade.php) :

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

4. <meta charset="utf-8">

5. <meta http-equiv="X-UA-Compatible" content="IE=edge">

6. <meta name="viewport" content="width=device-width, initial-scale=1">

7. <title>Les emails</title>

8. {!! Html::style('<a href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css</a>') !!}

9. {!! Html::style('<a href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css">https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css</a>') !!}

10. <!--[if lt IE 9]>

11. {{ Html::style('<a href="https://oss.maxcdn.com/libs/html5shiv/3.7.2/html5shiv.js">https://oss.maxcdn.com/libs/html5shiv/3.7.2/html5shiv.js</a>') }}

12. {{ Html::style('<a href="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js">https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js</a>') }}

13. <![endif]-->

14. </head>

15. <body>

16. @yield('contenu')

17. </body>

18. </html>

Voici la vue pour le formulaire(resources/views/email.blade.php) :

1. @extends('template')

3. @section('contenu')

4. <br>

5. <div class="col-sm-offset-4 col-sm-4">

6. <div class="panel panel-info">

7. <div class="panel-heading">Inscription à la lettre d'information</div>

8. <div class="panel-body">

9. {!! Form::open(['route' => 'store.email']) !!}

10. <div class="form-group {!! $errors->has('email') ? 'has-error' : '' !!}">

11. {!! Form::email('email', null, array('class' => 'form-control', 'placeholder' => 'Entrez votre email')) !!}

12. {!! $errors->first('email', '<small class="help-block">:message</small>') !!}

13. </div>

14. {!! Form::submit('Envoyer !', ['class' => 'btn btn-info pull-right']) !!}

15. {!! Form::close() !!}

16. </div>

17. </div>

18. </div>

19. @endsection

Cette vue ne présente aucune nouveauté pour vous si ce n'est l'utilisation du nom de la route, elle répond à l'URL (avec le verbe get) : http://monsite.fr/email.

L'aspect est le suivant :

Voici maintenant la vue de confirmation (resources/views/email_ok.blade.php) :

1. @extends('template')

3. @section('contenu')

4. <br>

5. <div class="col-sm-offset-3 col-sm-6">

6. <div class="panel panel-info">

7. <div class="panel-heading">Inscription à la lettre d'information</div>

8. <div class="panel-body">

9. Merci. Votre adresse a bien été prise en compte.

10. </div>

11. </div>

12. </div>

13. @endsection

Avec cet aspect :

I-F - Le fonctionnement

Voyons maintenant si tout se passe bien. Je soumets une adresse :

Je reçois la confirmation :

Je regarde dans la base :

Je soumets la même adresse :

Voyons un peu les requêtes générées par Eloquent avec par exemple la soumission de l'adresse toto@gui.com (vous les trouvez à la rubrique Queries de la barre de débogage) :

select count(*) as aggregate from `emails` where `email` = 'toto@gui.com'
insert into `emails` (`email`) values ('toto@gui.com')

La première requête est destinée à tester la présence éventuelle de l'adresse dans la table pour répondre à la règle « unique ». La seconde insère l'enregistrement dans la table. Vous voyez qu'Eloquent vous simplifie la tâche, vous n'avez pas besoin d'écrire les requêtes SQL, il le fait pour vous. Vous vous contentez de manipuler un objet.

N'hésitez pas à regarder les informations de la barre de débogage, vous y trouverez de précieux renseignements sur les requêtes (HTTP et SQL), les vues utilisées, les routes, les délais, les exceptions générées… Vous avez aussi un historique en cliquant sur la petite image de dossier :

I-G - Organisation du code

Maintenant posons-nous à nouveau la question de l'organisation du code. Dans le contrôleur nous avons mis la gestion du modèle :

$email = new Email;
$email->email = $request->email;
$email->save();

Autrement dit nous avons lié de façon étroite le contrôleur et le modèle. Supposons que nous fassions des modifications dans notre base de données et que nous placions l'email dans une autre table. Nous devrions évidemment intervenir dans le code du contrôleur pour tenir compte de cette modification.

Vous pouvez évidemment considérer que c'est peu probable, que la modification du code n'est pas très importante… Mais ici on a une application très simple, dans une situation réelle les utilisations de modèles sont nombreuses et alors la question devient bien plus pertinente.

I-G-1 - Première version

Dans ce cours je m'efforce de vous entraîner à prendre de bonnes habitudes. Plutôt que d'instancier directement une classe dans une autre, il vaut mieux une injection et laisser faire le conteneur. Regardez cette nouvelle version du contrôleur :

1. <?php

3. namespace App\Http\Controllers;

5. use App\Email;

6. use App\Http\Requests\EmailRequest;

8. class EmailController extends Controller

9. {

10. public function create()

11. {

12. return view('email');

13. }

15. public function store(EmailRequest $request, Email $email)

16. {

17. $email->email = $request->email;

18. $email->save();

20. return view('email_ok');

21. }

22. }

Maintenant le modèle est injecté dans la méthode, c'est plus élégant et efficace. Si jamais vous changez de modèle, vous n'avez plus qu'un changement de code limité sur le contrôleur. Mais ce n'est pas encore parfait.

I-G-2 - Seconde version

Dans l'idéal on veut que notre contrôleur ne soit pas du tout concerné par un changement dans la gestion des modèles. Voici une façon de procéder :

1. <?php

3. namespace App\Http\Controllers;

5. use App\Http\Requests\EmailRequest;

6. use App\Repositories\EmailRepository;

8. class EmailController extends Controller

9. {

10. public function create()

11. {

12. return view('email');

13. }

15. public function store(EmailRequest $request, EmailRepository $emailRepository)

16. {

17. $emailRepository->save($request->email);

19. return view('email_ok');

20. }

21. }

Maintenant j'injecte une classe de gestion qui possède la méthode save. Voici le contrat avec une interface (app/Repositories/EmailRepositoryInterface) :

1. <?php

3. namespace App\Repositories;

5. interface EmailRepositoryInterface

6. {

7. public function save($mail);

8. }

Et voici la classe qui implémente cette interface (app/Repositories/EmailRepository) :

1. <?php

3. namespace App\Repositories;

5. use App\Email;

7. class EmailRepository implements EmailRepositoryInterface

8. {

9. protected $email;

11. public function __construct(Email $email)

12. {

13. $this->email = $email;

14. }

16. public function save($mail)

17. {

18. $this->email->email = $mail;

19. $this->email->save();

20. }

21. }

Le modèle est injecté dans cette classe. Je l'ai injecté dans le constructeur pour généraliser la démarche en imaginant qu'on créera d'autres méthodes que l'on peut regrouper ici pour gérer les enregistrements. Le code est maintenant parfaitement organisé, facile à modifier et à tester.

Le Design Pattern Repository est un des plus répandus. Il permet de gérer la persistance des informations.

I-G-3 - Troisième version

On peut enfin, comme on l'a déjà vu, référencer l'interface plutôt que la classe, mais dans ce cas il faut informer le conteneur de la dépendance. Modifiez ainsi le fichier app/Http/Providers/AppServiceProvider.php :

1. public function register()

2. {

3. $this->app->bind(

4. 'App\Repositories\EmailRepositoryInterface',

5. 'App\Repositories\EmailRepository'

6. );

7. }

Et le contrôleur :

1. <?php

3. namespace App\Http\Controllers;

5. use App\Http\Requests\EmailRequest;

6. use App\Repositories\EmailRepositoryInterface;

8. class EmailController extends Controller

9. {

10. public function create()

11. {

12. return view('email');

13. }

15. public function store(EmailRequest $request, EmailRepositoryInterface $emailRepository)

16. {

17. $emailRepository->save($request->email);

19. return view('email_ok');

20. }

21. }

Maintenant, étant donné que le conteneur sait quelle classe instancier à partir de l'interface passée en paramètre, vous avez un code propre et facile à maintenir et à tester. Si vous changez d'avis sur la manière de stocker les emails, il vous suffit de décider d'instancier une autre classe à partir de l'interface, tant que le contrat passé avec le contrôleur ne change pas !

I-H - En résumé

- La base de données doit être configurée pour fonctionner avec Laravel.

- Les migrations permettent d'intervenir sur le schéma des tables de la base.

- Eloquent permet une représentation des tables sous forme d'objets pour simplifier les manipulations des enregistrements.

- Il est judicieux de prévoir la gestion du modèle dans une classe injectée dans le contrôleur.

- La barre de débogage donne de précieux renseignements sur les requêtes.

II - Les ressources (1/2)

Dans ce chapitre nous allons commencer à étudier les ressources qui permettent de créer des routes « CRUD » (Create, Read, Update, Delete) adaptées à la persistance de données. Comme exemple pratique nous allons prendre le cas d'une table d'utilisateurs, une situation qui se retrouve dans la plupart des applications.

II-A - Les données

II-A-1 - La migration

À partir d'un Laravel vierge, on va commencer par configurer la base comme on l'a vu au chapitre précédent et par installer la migration pour créer la table des migrations dans la base.

Lorsqu'on installe Laravel, on se retrouve avec deux migrations déjà présentes :

Pour le moment on va garder seulement la première qui concerne la création de la table des utilisateurs :

En voici le code :

1. <?php

3. use Illuminate\Support\Facades\Schema;

4. use Illuminate\Database\Schema\Blueprint;

5. use Illuminate\Database\Migrations\Migration;

7. class CreateUsersTable extends Migration

8. {

9. /**

10. * Run the migrations.

11. *

12. * @return void

13. */

14. public function up()

15. {

16. Schema::create('users', function (Blueprint $table) {

17. $table->increments('id');

18. $table->string('name');

19. $table->string('email')->unique();

20. $table->string('password');

21. $table->rememberToken();

22. $table->timestamps();

23. });

24. }

26. /**

27. * Reverse the migrations.

28. *

29. * @return void

30. */

31. public function down()

32. {

33. Schema::drop('users');

34. }

35. }

On a les champs :

- id : entier auto-incrémenté qui sera la clé primaire de la table ;

- name : texte pour le nom ;

- email : texte pour l'email (unique) ;

- password : texte pour le mot de passe (en fait une version cryptée du mot de passe) ;

- remember_token : généré par la méthode rememberToken qui sert pour l'authentification que nous verrons dans un chapitre ultérieur ;

- created_at et updated_at créés par la méthode timestamps.

Ensuite on lance la migration :

On doit se retrouver avec la table users créée :

Remarquez que le modèle User est déjà présent dans le dossier app quand vous installez Laravel parce qu'il est un peu particulier comme nous le verrons lorsque nous parlerons de l'authentification. Pour le moment nous allons nous satisfaire du fait qu'il existe déjà sans nous soucier de son contenu.

II-B - Une ressource

II-B-1 - Création

On va maintenant créer une ressource avec Artisan :

<samp>php artisan make:controller UserController --resource</samp>

C'est la commande qu'on a déjà vue pour créer un contrôleur avec en plus l'option -resource.

Vous trouvez comme résultat le contrôleur app/Http/Controllers/UserController :

1. <?php

3. namespace App\Http\Controllers;

5. use Illuminate\Http\Request;

7. use App\Http\Requests;

9. class UserController extends Controller

10. {

11. /**

12. * Display a listing of the resource.

13. *

14. * @return \Illuminate\Http\Response

15. */

16. public function index()

17. {

18. //

19. }

21. /**

22. * Show the form for creating a new resource.

23. *

24. * @return \Illuminate\Http\Response

25. */

26. public function create()

27. {

28. //

29. }

31. /**

32. * Store a newly created resource in storage.

33. *

34. * @param  \Illuminate\Http\Request  $request

35. * @return \Illuminate\Http\Response

36. */

37. public function store(Request $request)

38. {

39. //

40. }

42. /**

43. * Display the specified resource.

44. *

45. * @param  int  $id

46. * @return \Illuminate\Http\Response

47. */

48. public function show($id)

49. {

50. //

51. }

53. /**

54. * Show the form for editing the specified resource.

55. *

56. * @param  int  $id

57. * @return \Illuminate\Http\Response

58. */

59. public function edit($id)

60. {

61. //

62. }

64. /**

65. * Update the specified resource in storage.

66. *

67. * @param  \Illuminate\Http\Request  $request

68. * @param  int  $id

69. * @return \Illuminate\Http\Response

70. */

71. public function update(Request $request, $id)

72. {

73. //

74. }

76. /**

77. * Remove the specified resource from storage.

78. *

79. * @param  int  $id

80. * @return \Illuminate\Http\Response

81. */

82. public function destroy($id)

83. {

84. //

85. }

86. }

Les sept méthodes créées couvrent la gestion complète des utilisateurs :

- index : pour afficher la liste des utilisateurs ;

- create : pour envoyer le formulaire pour la création d'un nouvel utilisateur ;

- store : pour créer un nouvel utilisateur ;

- show : pour afficher les données d'un utilisateur ;

- edit : pour envoyer le formulaire pour la modification d'un utilisateur ;

- update : pour modifier les données d'un utilisateur ;

- destroy : pour supprimer un utilisateur.

II-C - Les routes

Pour créer toutes les routes, il suffit de cette unique ligne de code :

Route::resource('user', 'UserController');

On va vérifier ces routes avec Artisan :

Vous trouvez sept routes, avec chacune une méthode et une URL, qui pointent sur les sept méthodes du contrôleur. Notez également que chaque route a aussi un nom qui peut être utilisé par exemple pour une redirection. On retrouve aussi pour chaque route le middleware web dont je vous ai déjà parlé.

Le middleware web est automatiquement ajouté à toutes les routes.

Nous allons à présent considérer chacune de ces routes et créer la gestion des données, les vues, et le code nécessaire au niveau du contrôleur.

On peut limiter le nombre de routes si on n'a pas besoin de les avoir toutes.

II-D - Le contrôleur

Il nous faut à présent coder le contrôleur. Pour rester dans la démarche de bonne organisation des classes, je vais continuer à limiter le contrôleur à la réception des requêtes et l'envoi des réponses. Il va donc falloir injecter :

- la validation ;

- la gestion des données.

Pour la validation on va avoir deux cas :

- la création d'un utilisateur avec vérification de l'unicité du nom et de l'email et la conformité du mot de passe ;

- la modification d'un utilisateur, avec la même vérification d'unicité, mais en excluant l'enregistrement en cours de modification. D'autre part nous n'allons pas inclure le mot de passe dans cette modification.

On va donc avoir besoin de deux requêtes de formulaire : une pour la création et l'autre pour la modification.

On pourrait se contenter d'une seule requête de formulaire en discriminant grâce à la méthode ou à la constitution de l'URL. Mais il me semble plus clair d'en utiliser deux.

Pour la gestion une seule classe UserRepository suffira.

Si on considère ces injections, voici le code du contrôleur :

1. <?php

3. namespace App\Http\Controllers;

5. use Illuminate\Http\Request;

6. use App\Http\Requests\UserCreateRequest;

7. use App\Http\Requests\UserUpdateRequest;

8. use App\Repositories\UserRepository;

9. use App\User;

11. class UserController extends Controller

12. {

13. protected $userRepository;

14. protected $nbrPerPage = 4;

16. public function __construct(UserRepository $userRepository)

17. {

18. $this->userRepository = $userRepository;

19. }

21. public function index()

22. {

23. $users = $this->userRepository->getPaginate($this->nbrPerPage);

25. return view('index', compact('users'));

26. }

28. public function create()

29. {

30. return view('create');

31. }

33. public function store(UserCreateRequest $request)

34. {

35. $user = $this->userRepository->store($request->all());

37. return redirect()->route('user.index')->withOk("L'utilisateur " . $user->name . " a été créé.");

38. }

40. public function show(User $user)

41. {

42. return view('show', compact('user'));

43. }

45. public function edit(User $user)

46. {

47. return view('edit', compact('user'));

48. }

50. public function update(UserUpdateRequest $request, User $user)

51. {

52. $this->userRepository->update($user, $request->all());

54. return redirect()->route('user.index')->withOk("L'utilisateur " . $request->name . " a été modifié.");

55. }

57. public function destroy(User $user)

58. {

59. $this->userRepository->destroy($user);

61. return back();

62. }

63. }

Nous allons évidemment analyser tout ça dans le détail, mais globalement vous voyez que le code est très épuré :

- réception de la requête ;

- délégation du traitement si nécessaire (validation et gestion) ;

- envoi de la réponse.

II-D-1 - Liaison implicite

Il me faut toutefois préciser déjà un point important. Dans la version du contrôleur générée par défaut, on voit que l'utilisateur au niveau des arguments des fonctions est référencé par son identifiant, par exemple :

public function show($id)

La variable id contient la valeur passée dans l'URL. Par exemple …/user/8 indique qu'on veut voir les informations de l'utilisateur d'identifiant 8. Il suffit donc ensuite d'aller chercher dans la base l'utilisateur correspondant.

Si vous regardez la signature dans le contrôleur définitif, vous avez ceci :

public function show(User $user)

L'argument cette fois est une instance du modèle App\User. Étant donné qu'il rencontre ce type Laravel va automatiquement livrer une instance du modèle pour l'utilisateur concerné ! C'est ce qu'on appelle liaison implicite (Implicit Bindind). Vous voyez encore là à quel point Laravel nous simplifie la vie.

II-E - La validation

II-E-1 - Création d'un utilisateur

On va créer une requête de formulaire pour la création d'un utilisateur. Je ne vous réitère pas la démarche de création avec Artisan parce que nous l'avons déjà vue plusieurs fois. Voici le code de la classe :

1. <?php

3. namespace App\Http\Requests;

5. use Illuminate\Foundation\Http\FormRequest;

7. class UserCreateRequest extends FormRequest

8. {

9. /**

10. * Determine if the user is authorized to make this request.

11. *

12. * @return bool

13. */

14. public function authorize()

15. {

16. return true;

17. }

19. /**

20. * Get the validation rules that apply to the request.

21. *

22. * @return array

23. */

24. public function rules()

25. {

26. return [

27. 'name' => 'bail|required|max:255',

28. 'email' => 'bail|required|email|max:255|unique:users',

29. 'password' => 'bail|required|confirmed|min:8'

30. ];

31. }

32. }

Avec ces règles :

- bail : on s'arrête à la première erreur ;

- name : requis, longueur maximale de 255 caractères ;

- email : requis, adresse valide, longueur maximale de 255 caractères, et unique dans la table users ;

- password : requis, longueur minimale de 8 caractères et doit correspondre à ce qui est entré dans le champ de confirmation du mot de passe (confirmed).

II-E-2 - Modification d'un utilisateur

Pour la modification d'un utilisateur, nous allons avoir un petit souci. En effet on veut conserver l'unicité de l'email dans la base, il est donc judicieux de prévoir une règle « unique ». Mais comme la valeur de l'email existant est déjà dans la base, on va avoir un échec de la validation en cas de non-modification de cette valeur, ce qui est probable.

Comment nous en sortir ?

Étant donné que nous disposons d'une fonction, on peut effectuer tous les traitements que l'on veut. Voici alors la requête de formulaire pour la modification d'un utilisateur :

1. <?php

3. namespace App\Http\Requests;

5. use Illuminate\Foundation\Http\FormRequest;

7. class UserUpdateRequest extends FormRequest

8. {

9. /**

10. * Determine if the user is authorized to make this request.

11. *

12. * @return bool

13. */

14. public function authorize()

15. {

16. return true;

17. }

19. /**

20. * Get the validation rules that apply to the request.

21. *

22. * @return array

23. */

24. public function rules()

25. {

26. return [

27. 'name' => 'bail|required|max:255',

28. 'email' => 'bail|required|email|max:255|unique:users,email,' . $this->user->id,

29. ];

30. }

31. }

On récupère l'instance du modèle pour l'utilisateur dans l'URL (on sait qu'il y a une liaison implicite). Ensuite on utilise une possibilité d'exclusion de la règle « unique ».

Vous devez donc avoir ces deux fichiers pour la validation :

Dans le prochain chapitre nous poursuivrons l'étude de cette ressource avec la gestion des données et les vues.‌

II-F - En résumé

- Lors de la migration, le constructeur de schéma permet de fixer toutes les propriétés des champs.

- Une ressource dans Laravel est constituée d'un contrôleur comportant les 7 méthodes permettant une gestion complète.

- Les routes vers une ressource sont créées avec une simple ligne de code.

- On peut mettre en place dans le routage une liaison implicite pour générer automatiquement une instance de la classe dont l'identifiant est passé dans l'URL.

- Pour une ressource la validation est toujours différente entre la création et la modification et il faut adapter le code pour en tenir compte.

III - Les ressources (2/2) et les erreurs

Dans ce chapitre nous allons poursuivre notre étude de la ressource pour les utilisateurs. Nous avons au chapitre précédent passé en revue la migration, les routes, le contrôleur et la validation. Il nous reste maintenant à voir la gestion des données, les vues, et aussi comment tout cela s'articule pour fonctionner.

III-A - Le gestionnaire de données (repository)

Nous avons vu que nous injectons un gestionnaire de données dans le contrôleur en plus des deux classes de validation :

1. public function __construct(UserRepository $userRepository)

2. {

3. $this->userRepository = $userRepository;

4. }

Ce gestionnaire est chargé de toutes les actions au niveau de la table des utilisateurs. Voici son code (app/Repositories/UserRepository.php) :

1. <?php

3. namespace App\Repositories;

5. use App\User;

7. class UserRepository

8. {

9. protected $user;

11. public function __construct(User $user)

12. {

13. $this->user = $user;

14. }

16. public function getPaginate($n)

17. {

18. return $this->user->paginate($n);

19. }

21. public function store(Array $inputs)

22. {

23. $inputs['password'] = bcrypt($inputs['password']);

24. return $this->user->create($inputs);

25. }

27. public function update(User $user, Array $inputs)

28. {

29. $user->update($inputs);

30. }

32. public function destroy(User $user)

33. {

34. $user->delete();

35. }

36. }

Vous devez vous retrouver avec ce fichier dans le dossier Repositories :

Le repository dans notre exemple est évidemment très léger et on pourrait se questionner sur sa pertinence. Par exemple quel intérêt d'appliquer la méthode delete dans le repository alors qu'on pourrait directement le faire sur le modèle qu'on a déjà dans le contrôleur ? On verra que ça deviendra plus évident quand on augmente la complexité d'une application. Il y a déjà eu des débats animés sur les repositories et le sujet n'est pas clos. Dans ce cours, je privilégie leur utilisation, ensuite libre à vous de tout coder dans les contrôleurs ou les modèles. L'important est de trouver une organisation du code qui vous convienne et qui reste lisible si vous devez le partager.

III-B - Le fonctionnement

Nous allons à présent analyser ses différentes actions en voyant comment sont articulés le contrôleur et le repository.

III-B-1 - index

La page d'accueil (index) répond à l'URL …/user avec la méthode GET.

On arrive dans la méthode index du contrôleur :

1. public function index()

2. {

3. $users = $this->userRepository->getPaginate($this->nbrPerPage);

5. return view('index', compact('users'));

6. }

Il nous faut ici les renseignements sur les utilisateurs à afficher. Comme il peut y en avoir beaucoup, on va limiter l'affichage en faisant de la pagination. La propriété nbrPerPage contient le nombre d'utilisateurs à afficher sur chaque page. On demande au repository de renvoyer ces informations dans la variable $users.

Dans le repository on a ce code :

1. public function __construct(User $user)

2. {

3. $this->user = $user;

4. }

6. public function getPaginate($n)

7. {

8. return $this->user->paginate($n);

9. }

Dès qu'on instancie la classe on définit une propriété user de type App\User.

Dans la méthode getPaginate on utilise la méthode paginate d'Eloquent pour renvoyer les informations paginées. Encore une fois c'est Eloquent qui s'occupe de tout !

III-B-2 - show

La page pour voir les informations d'un utilisateur (show) répond à l'URL …/user/id avec la méthode GET.

On arrive dans la méthode show du contrôleur :

1. public function show(User $user)

2. {

3. return view('show', compact('user'));

4. }

Étant donné qu'on a la liaison implicite, on n'a même pas besoin du repository, on envoie directement les informations dans la vue.

III-B-3 - create

La page pour voir le formulaire de création d'un utilisateur (create) répond à l'URL …/user/create avec la méthode GET.

On arrive dans la méthode create du contrôleur :

1. public function create()

2. {

3. return view('create');

4. }

Ici on a juste une vue à retourner et aucune information à transmettre.

III-B-4 - store

Pour recevoir la soumission du formulaire de création d'un utilisateur (store) on a l'URL …/user avec la méthode POST.

On arrive dans la méthode store du contrôleur :

1. public function store(UserCreateRequest $request)

2. {

3. $user = $this->userRepository->store($request->all());

5. return redirect()->route('user.index')->withOk("L'utilisateur " . $user->name . " a été créé.");

6. }

On fait appel au repository avec sa méthode store en lui transmettant toutes les informations du formulaire ($request->all()).

Ensuite on redirige sur la route user.index en flashant une information en session (ce qui veut dire qu'elle reste mémorisée seulement pour la prochaine requête) avec les renseignements à afficher dans la vue, en l'occurrence ce sera une alerte informative.

Dans le repository on a ce code :

1. public function store(Array $inputs)

2. {

3. $inputs['password'] = bcrypt($inputs['password']);

4. return $this->user->create($inputs);

5. }

On commence par crypter le mot de passe (bcrypt). Et ensuite on utilise la méthode create d'Eloquent pour créer l'utilisateur.

Par sécurité ce type d'assignement de masse (on transmet directement un tableau de valeurs issues du client) est limité par une propriété au niveau du modèle qui désigne précisément les noms des colonnes susceptibles d'être modifiées. Si vous regardez dans le fichier app/User.php vous trouvez cette propriété :

protected $fillable = ['name', 'email', 'password',];

Ce sont les seules colonnes qui seront impactées par la méthode create (et équivalentes). Attention à cela lorsque vous avez un bogue mystérieux avec des colonnes qui ne se mettent pas à jour !

III-B-5 - edit

La page pour voir le formulaire de modification d'un utilisateur (edit) répond à l'URL …/user/id avec la méthode GET.

On arrive dans la méthode edit du contrôleur :

1. public function edit(User $user)

2. {

3. return view('edit',  compact('user'));

4. }

Comme pour show, étant donné qu'on a la liaison implicite on n'a même pas besoin du repository, on envoie directement les informations dans la vue.

III-B-6 - update

Pour gérer la soumission du formulaire de modification d'un utilisateur (update) on a l'URL …/user/id avec la méthode PUT.

On arrive dans la méthode update du contrôleur :

1. public function update(UserUpdateRequest $request, User $user)

2. {

3. $this->userRepository->update($user, $request->all());

5. return redirect()->route('user.index')->withOk("L'utilisateur " . $request->name . " a été modifié.");

6. }

On fait appel à la méthode update du repository en lui transmettant :

- une instance de App\User pour l'utilisateur (liaison implicite) ;

- toutes les données du formulaire ($request->all()).

Ensuite on redirige sur la page d'accueil (index) en flashant une information en session (ce qui veut dire qu'elle reste mémorisée seulement pour la prochaine requête) avec les renseignements à afficher dans la vue, en l'occurrence ce sera une alerte informative.

III-B-7 - destroy

Pour supprimer un utilisateur (destroy) on a l'URL …/user/id avec la méthode DELETE.

On arrive dans la méthode destroy du contrôleur :

1. public function destroy(User $user)

2. {

3. $this->userRepository->destroy($user);

5. return back();

6. }

On fait appel à la méthode destroy du repository en lui transmettant le modèle généré par la liaison implicite :

1. public function destroy(User $user)

2. {

3. $user->delete();

4. }

C'est la méthode delete d'Eloquent qui supprime effectivement l'enregistrement.

III-C - Le template

Nous aurons le même template pour toutes les vues, c'est celui que nous avons déjà utilisé dans les précédents chapitres (resources/views/template.blade.php) :

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

4. <meta charset="utf-8">

5. <meta http-equiv="X-UA-Compatible" content="IE=edge">

6. <meta name="viewport" content="width=device-width, initial-scale=1">

7. <title>Lettre d'information</title>

8. {!! Html::style('<a href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css</a>') !!}

9. {!! Html::style('<a href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css">https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css</a>') !!}

10. <!--[if lt IE 9]>

11. {{ Html::style('<a href="https://oss.maxcdn.com/libs/html5shiv/3.7.2/html5shiv.js">https://oss.maxcdn.com/libs/html5shiv/3.7.2/html5shiv.js</a>') }}

12. {{ Html::style('<a href="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js">https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js</a>') }}

13. <![endif]-->

14. </head>

15. <body>

16. @yield('contenu')

17. </body>

18. </html>

III-D - La vue index

Cette vue (resources/views/index.blade.php) est destinée à afficher la liste paginée des utilisateurs avec des boutons pour pouvoir accomplir toutes les actions. Voici le code de cette vue :

1. @extends('template')

3. @section('contenu')

4. <br>

5. <div class="col-sm-offset-3 col-sm-6">

6. @if(session()->has('ok'))

7. <div class="alert alert-success alert-dismissible">{!! session('ok') !!}</div>

8. @endif

9. <div class="panel panel-primary">

10. <div class="panel-heading">

11. <h3 class="panel-title">Liste des utilisateurs</h3>

12. </div>

13. <table class="table">

14. <thead>

15. <tr>

16. <th>#</th>

17. <th>Nom</th>

18. <th></th>

19. <th></th>

20. <th></th>

21. </tr>

22. </thead>

23. <tbody>

24. @foreach ($users as $user)

25. <tr>

26. <td>{!! $user->id !!}</td>

27. <td class="text-primary"><strong>{!! $user->name !!}</strong></td>

28. <td>{!! link_to_route('user.show', 'Voir', [$user->id], ['class' => 'btn btn-success btn-block']) !!}</td>

29. <td>{!! link_to_route('user.edit', 'Modifier', [$user->id], ['class' => 'btn btn-warning btn-block']) !!}</td>

30. <td>

31. {!! Form::open(['method' => 'DELETE', 'route' => ['user.destroy', $user->id]]) !!}

32. {!! Form::submit('Supprimer', ['class' => 'btn btn-danger btn-block', 'onclick' => 'return confirm(\'Vraiment supprimer cet utilisateur ?\')']) !!}

33. {!! Form::close() !!}

34. </td>

35. </tr>

36. @endforeach

37. </tbody>

38. </table>

39. </div>

40. {!! link_to_route('user.create', 'Ajouter un utilisateur', [], ['class' => 'btn btn-info pull-right']) !!}

41. {!! $users->links() !!}

42. </div>

43. @endsection

III-D-1 - Structures de contrôle de Blade

Nous découvrons de nouvelles possibilités de Blade avec la structure de contrôle @if :

@if(session()->has('ok'))
<div class="alert alert-success alert-dismissible">{!! session('ok') !!}</div>
@endif

On affiche l'alerte que si on a une clé ok en session.

On trouve aussi la structure @foreach qui permet d'itérer dans une collection (ici les utilisateurs) :

@foreach ($users as $user)
...
@endforeach

III-D-2 - Helper link_to_route

Remarquez également l'utilisation de l'helper link_to_route pour créer un lien :

<td>{!! link_to_route('user.show', 'Voir', [$user->id], ['class' => 'btn btn-success btn-block']) !!}</td>

On précise le nom de la route (user.show) , le titre ('Voir'), un paramètre ($user->id) et un attribut (classes de Bootstrap pour obtenir une apparence de bouton).

III-D-3 - Formulaire pour la suppression

Pour supprimer un utilisateur, on doit envoyer une requête avec la méthode DELETE. Les navigateurs ne savent pas faire ça alors on prévoit un formulaire :

{!! Form::open(['method' => 'DELETE', 'route' => ['user.destroy', $user->id]]) !!}
{!! Form::submit('Supprimer', ['class' => 'btn btn-danger btn-block', 'onclick' => 'return confirm(\'Vraiment supprimer cet utilisateur ?\')']) !!}
{!! Form::close() !!}

Remarquez la possibilité dans l'ouverture du formulaire de désigner une route (user.destroy) et un paramètre ($user->id).

Enfin voici l'aspect de la vue :

III-E - La vue show

La vue show sert à afficher la fiche d'un utilisateur avec son nom et son adresse email :

1. @extends('template')

3. @section('contenu')

4. <div class="col-sm-offset-4 col-sm-4">

5. <br>

6. <div class="panel panel-primary">

7. <div class="panel-heading">Fiche d'utilisateur</div>

8. <div class="panel-body">

9. <p>Nom : {{ $user->name }}</p>

10. <p>Email : {{ $user->email }}</p>

11. </div>

12. </div>

13. <a href="javascript:history.back()" class="btn btn-primary">

14. <span class="glyphicon glyphicon-circle-arrow-left"></span> Retour

15. </a>

16. </div>

17. @endsection

Le code ne présente aucune particularité.

Voici son aspect :

III-F - La vue edit

La vue edit sert à la modification d'un utilisateur, elle affiche un formulaire :

1. @extends('template')

3. @section('contenu')

4. <div class="col-sm-offset-4 col-sm-4">

5. <br>

6. <div class="panel panel-primary">

7. <div class="panel-heading">Modification d'un utilisateur</div>

8. <div class="panel-body">

9. <div class="col-sm-12">

10. {!! Form::model($user, ['route' => ['user.update', $user->id], 'method' => 'put', 'class' => 'form-horizontal panel']) !!}

11. <div class="form-group {!! $errors->has('name') ? 'has-error' : '' !!}">

12. {!! Form::text('name', null, ['class' => 'form-control', 'placeholder' => 'Nom']) !!}

13. {!! $errors->first('name', '<small class="help-block">:message</small>') !!}

14. </div>

15. <div class="form-group {!! $errors->has('email') ? 'has-error' : '' !!}">

16. {!! Form::email('email', null, ['class' => 'form-control', 'placeholder' => 'Email']) !!}

17. {!! $errors->first('email', '<small class="help-block">:message</small>') !!}

18. </div>

19. {!! Form::submit('Envoyer', ['class' => 'btn btn-primary pull-right']) !!}

20. {!! Form::close() !!}

21. </div>

22. </div>

23. </div>

24. <a href="javascript:history.back()" class="btn btn-primary">

25. <span class="glyphicon glyphicon-circle-arrow-left"></span> Retour

26. </a>

27. </div>

28. @endsection

Remarquez l'utilisation de Form::model :

{!! Form::model($user, ['route' => ['user.update', $user->id], 'method' => 'put', 'class' => 'form-horizontal panel']) !!}

Cette méthode permet de lier le formulaire au modèle et ainsi de renseigner automatiquement les contrôles qui possèdent le même nom qu'un champ de l'enregistrement.

Voici son aspect :

En cas d'erreur dans la validation, on a l'affichage correspondant :

III-G - La vue create

Cette vue sert à afficher le formulaire pour créer un utilisateur, c'est quasiment la même que pour la modification avec le mot de passe en plus :

1. @extends('template')

3. @section('contenu')

4. <div class="col-sm-offset-4 col-sm-4">

5. <br>

6. <div class="panel panel-primary">

7. <div class="panel-heading">Création d'un utilisateur</div>

8. <div class="panel-body">

9. <div class="col-sm-12">

10. {!! Form::open(['route' => 'user.store', 'class' => 'form-horizontal panel']) !!}

11. <div class="form-group {!! $errors->has('name') ? 'has-error' : '' !!}">

12. {!! Form::text('name', null, ['class' => 'form-control', 'placeholder' => 'Nom']) !!}

13. {!! $errors->first('name', '<small class="help-block">:message</small>') !!}

14. </div>

15. <div class="form-group {!! $errors->has('email') ? 'has-error' : '' !!}">

16. {!! Form::email('email', null, ['class' => 'form-control', 'placeholder' => 'Email']) !!}

17. {!! $errors->first('email', '<small class="help-block">:message</small>') !!}

18. </div>

19. <div class="form-group {!! $errors->has('password') ? 'has-error' : '' !!}">

20. {!! Form::password('password', ['class' => 'form-control', 'placeholder' => 'Mot de passe']) !!}

21. {!! $errors->first('password', '<small class="help-block">:message</small>') !!}

22. </div>

23. <div class="form-group">

24. {!! Form::password('password_confirmation', ['class' => 'form-control', 'placeholder' => 'Confirmation mot de passe']) !!}

25. </div>

26. {!! Form::submit('Envoyer', ['class' => 'btn btn-primary pull-right']) !!}

27. {!! Form::close() !!}

28. </div>

29. </div>

30. </div>

31. <a href="javascript:history.back()" class="btn btn-primary">

32. <span class="glyphicon glyphicon-circle-arrow-left"></span> Retour

33. </a>

34. </div>

35. @endsection

Au niveau de la validation, on a pour le mot de passe deux contrôles pour avoir une double saisie, ce qui est classique. Il faut évidemment vérifier la concordance des deux. Dans la requête de formulaire, c'est prévu ainsi :

'password' => 'bail|required|confirmed|min:8'

On a mis confirmed.

Mais comment Laravel connaît-il le nom du contrôle qui sert pour la confirmation ?

Par convention il cherche un contrôle dont le nom est celui du contrôle primaire auquel on ajoute « _confirmation ». Ici comme le contrôle s'appelle password, le contrôle pour la confirmation porte le nom de password_confirmation.

Si vous faites un essai avec deux entrées différentes, vous devriez obtenir ce message :

Vous devez donc avoir ces cinq vues :

III-H - Les erreurs

La gestion des erreurs constitue une part importante dans le développement d'une application. On dit parfois que c'est dans ce domaine qu'on fait la différence entre le professionnel et l'amateur. Que nous propose Laravel dans ce domaine ?

Puisque nous sommes dans l'accès aux données, il peut arriver un souci avec la connexion à la base. Voyons un peu ce que nous obtenons si MySQL ne répond plus… Avec notre application si j'arrête le service de MySQL puis que je veux ouvrir la page d'accueil :

Je n'ai affiché ici que la partie supérieure.

Ne soyez pas impressionné par la quantité de messages de la page d'erreurs, avec un peu d'habitude vous serez heureux de disposer de tous ces renseignements lorsqu'une erreur se produit dans votre application.

On a le détail parce qu'on est en mode de débogage (variable APP_DEBUG avec la valeur true dans le fichier .env). En mode production on aura seulement la partie supérieure du message qui dit laconiquement que quelque chose ne va pas.

On a déjà vu qu'on peut mettre dans le dossier views/errors une vue avec comme nom le code de l'erreur, on l'a déjà fait pour l'erreur 404. Ici on a un code d'erreur 500 (erreur interne du serveur), on pourrait donc créer une vue views/errors/500.blade.php, mais elle serait commune à toutes les erreurs 500.

Pour personnaliser la page d'erreurs de façon plus fine, il faut aller un peu toucher au fichier app/Exceptions/Handler.php :

Voici son code :

1. <?php

3. namespace App\Exceptions;

5. use Exception;

6. use Illuminate\Auth\AuthenticationException;

7. use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

9. class Handler extends ExceptionHandler

10. {

11. /**

12. * A list of the exception types that should not be reported.

13. *

14. * @var array

15. */

16. protected $dontReport = [

17. \Illuminate\Auth\AuthenticationException::class,

18. \Illuminate\Auth\Access\AuthorizationException::class,

19. \Symfony\Component\HttpKernel\Exception\HttpException::class,

20. \Illuminate\Database\Eloquent\ModelNotFoundException::class,

21. \Illuminate\Session\TokenMismatchException::class,

22. \Illuminate\Validation\ValidationException::class,

23. ];

25. /**

26. * Report or log an exception.

27. *

28. * This is a great spot to send exceptions to Sentry, Bugsnag, etc.

29. *

30. * @param  \Exception  $exception

31. * @return void

32. */

33. public function report(Exception $exception)

34. {

35. parent::report($exception);

36. }

38. /**

39. * Render an exception into an HTTP response.

40. *

41. * @param  \Illuminate\Http\Request  $request

42. * @param  \Exception  $exception

43. * @return \Illuminate\Http\Response

44. */

45. public function render($request, Exception $exception)

46. {

47. return parent::render($request, $exception);

48. }

50. /**

51. * Convert an authentication exception into an unauthenticated response.

52. *

53. * @param  \Illuminate\Http\Request  $request

54. * @param  \Illuminate\Auth\AuthenticationException  $exception

55. * @return \Illuminate\Http\Response

56. */

57. protected function unauthenticated($request, AuthenticationException $exception)

58. {

59. if ($request->expectsJson()) {

60. return response()->json(['error' => 'Unauthenticated.'], 401);

61. }

63. return redirect()->guest('login');

64. }

65. }

Toutes les erreurs d'exécution sont traitées ici. On découvre que tout est archivé avec la méthode report.

Mais où se trouve cet archivage ?

Regardez dans le dossier storage/logs, vous trouvez un fichier laravel.log. En l'ouvrant, vous trouvez les erreurs archivées avec leur trace. Par exemple dans notre cas on a bien :

[2016-09-01 15:27:27] local.ERROR: PDOException: SQLSTATE[HY000] [2002] Aucune connexion n'a pu être établie, car l'ordinateur cible l'a expressément refusée.

C'est important de disposer de ce genre d'archivage des erreurs sur une application en production.

On a vu plus haut que lorsqu'on arrête le service MySql on génère une erreur PDOException.

‌Comment intercepter cette erreur ?

Voilà une solution :

1. public function render($request, Exception $exception)

2. {

3. if($exception instanceof \PDOException) {

4. return response()->view('errors.pdo', [], 500);

5. }

7. return parent::render($request, $exception);

8. }

Il suffit maintenant de créer une vue resources/views/error/pdo.blade.php :

1. @extends('template')

3. @section('contenu')

4. <br>

5. <div class="col-sm-offset-4 col-sm-4">

6. <div class="panel panel-danger">

7. <div class="panel-heading">

8. <h3 class="panel-title">Il y a un problème !</h3>

9. </div>

10. <div class="panel-body">

11. <p>Notre base de données semble inaccessible pour le moment.</p>

12. <p>Veuillez nous en excuser.</p>

13. </div>

14. </div>

15. </div>

16. @endsection

Maintenant on va afficher ceci :

C'est quand même plus explicite et élégant !

III-I - En résumé

- Créer un gestionnaire (repository) indépendant du contrôleur pour les accès aux données permet de disposer d'un code clair et facile à maintenir et tester.

- Les vues doivent utiliser au maximum les possibilités de Blade, des helpers et de la classe Form pour être concises et lisibles.

- La gestion des erreurs ne doit pas être négligée, il faut enlever le mode de débogage sur un site en production et prévoir des messages explicites pour les utilisateurs en fonction de l'erreur rencontrée.

IV - L'authentification

L'authentification constitue une tâche fréquente. En effet il y a souvent des parties d'un site qui ne doivent être accessibles qu'à certains utilisateurs, ne serait-ce que l'administration. La solution proposée par Laravel est d'une grande simplicité parce que tout est déjà préparé comme nous allons le voir dans ce chapitre.

IV-A - La base de données

Par défaut la partie persistance de l'authentification (c'est-à-dire la manière de retrouver les renseignements des utilisateurs) avec Laravel se fait en base de données avec Eloquent et part du principe qu'il y a un modèle App\User (dans le dossier app).

Lors de l'installation, il existe deux migrations présentes :

Repartez d'une installation vierge et faites la migration avec Artisan :

Vous devriez normalement obtenir ces trois tables :

Je rappelle que la table migrations sert seulement d'intendance pour les migrations et que vous ne devez pas y toucher.

IV-A-1 - La table users

Par défaut Laravel considère que cette table existe et il s'en sert comme référence pour l'authentification. On a déjà créé et utilisé cette table dans le chapitre précédent.

IV-A-2 - La table password_reset

Lors des précédents chapitres, je vous ai fait supprimer la migration pour cette table parce qu'on avait juste besoin de la table des utilisateurs. Pour ce chapitre on va avoir besoin de la seconde table qui va nous servir pour la réinitialisation des mots de passe.

IV-B - Les middlewares

Je vous ai déjà parlé des middlewares qui servent à effectuer un traitement à l'arrivée (ou au départ) des requêtes HTTP. On a vu le middleware de groupe web qui intervient pour toutes les requêtes qui arrivent. Dans ce chapitre on va voir deux autres middlewares qui vont nous permettre de savoir si un utilisateur est authentifié ou pas pour agir en conséquence.

IV-B-1 - Middleware auth

Le middleware auth permet de n'autoriser l'accès qu'aux utilisateurs authentifiés. Ce middleware est déjà présent et déclaré dans app\Http\Kernel.php :

1. protected $routeMiddleware = [

2. 'auth' => \Illuminate\Auth\Middleware\Authenticate::class,

3. ...

4. ];

On voit que la classe se trouve dans le framework et on ne va donc pas pouvoir le modifier directement si on veut changer son fonctionnement.

On peut utiliser ce middleware directement sur une route :

Route::get('comptes', function() {
// Réservé aux utilisateurs authentifiés
})->middleware('auth');

Ou un groupe de routes :

1. Route::group(['middleware' => 'auth'], function () {

2. Route::get('/', function () {

3. // Réservé aux utilisateurs authentifiés

4. });

5. Route::get('comptes', function () {

6. // Réservé aux utilisateurs authentifiés

7. });

8. });

Ou dans le constructeur d'un contrôleur :

1. public function __construct()

2. {

3. $this->middleware('auth');

4. }

Dans ce cas on peut désigner les méthodes concernées avec only ou non concernées avec except :

1. public function __construct()

2. {

3. $this->middleware('auth')->only(['create', 'update']);

4. }

IV-B-2 - Middleware guest

Ce middleware est exactement l'inverse du précédent : il permet de n'autoriser l'accès qu'aux utilisateurs non authentifiés. Ce middleware est aussi déjà présent et déclaré dans app\Http\Kernel.php :

1. protected $routeMiddleware = [

2. ...

3. 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,

4. ...

5. ];

Cette fois la classe se trouve dans l'application :

Pourquoi celui-ci est-il dans l'application ?

Pour comprendre pourquoi c'est le cas, regardez le code :

1. <?php

3. namespace App\Http\Middleware;

5. use Closure;

6. use Illuminate\Support\Facades\Auth;

8. class RedirectIfAuthenticated

9. {

10. /**

11. * Handle an incoming request.

12. *

13. * @param  \Illuminate\Http\Request  $request

14. * @param  \Closure  $next

15. * @param  string|null  $guard

16. * @return mixed

17. */

18. public function handle($request, Closure $next, $guard = null)

19. {

20. if (Auth::guard($guard)->check()) {

21. return redirect('/home');

22. }

24. return $next($request);

25. }

26. }

Si l'utilisateur est authentifié, on fait une redirection vers la route « /home ». Si la classe était dans le framework, on aurait du mal à changer cette URL ! Alors que là c'est accessible et modifiable.

De la même manière que auth, le middleware guest peut s'utiliser sur une route, un groupe de routes ou dans le constructeur d'un contrôleur, avec la même syntaxe.

IV-C - Les routes de l'authentification

Dans l'installation de base, vous ne trouvez aucune route pour l'authentification. Pour les créer (et ça ne créera pas seulement les routes), il y a une commande d'Artisan.

Regardez ce qui a été ajouté dans le fichier routes/web.php :

Auth::routes();

Route::get('/home', 'HomeController@index');

Apparemment il a aussi été créé le contrôleur HomeController :

Voyons quelles sont les routes générées (vous devez maintenant bien connaître la commande correspondante d'Artisan) :

Vous voyez que Auth::routes() produit pas mal de routes !

Vous pouvez voir également l'utilisation des deux middlewares dont je vous ai parlé plus haut.

Nous allons voir bientôt tout ça en action.

IV-D - Les vues de l'authentification

Il y a également eu la génération de vues :

On va aussi voir à quoi servent toutes ces vues.

IV-E - L'enregistrement d'un utilisateur

Commençons par le commencement avec l'enregistrement d'un utilisateur. Si vous allez sur la page d'accueil, vous allez remarquer la présence en haut à droite de la page de deux liens :

On trouve ce code dans la page d'accueil :

1. @if (Route::has('login'))

2. <div class="top-right links">

3. <a href="{{ URL('/login') }}">Login</a>

4. <a href="{{ URL('/register') }}">Register</a>

5. </div>

6. @endif

Avec un @if on vérifie si dans les routes (Route) on a (has) une route « login » et si c'est le cas on génère les deux liens.

Si on clique sur Register, on appelle la méthode showRegistrationForm du contrôleur RegisterController.

Remarquez au passage la déclaration du middleware guest dans ce contrôleur :

1. public function __construct()

2. {

3. $this->middleware('guest');

4. }

En effet si on est authentifié c'est qu'on n'a pas besoin de s'enregistrer !

Si vous regardez dans ce contrôleur, vous n'allez pas trouver la méthode showRegistrationForm , par contre vous allez trouver un trait :

use RegistersUsers;

L'espace de noms complet est : Illuminate\Foundation\Auth\RegistersUsers. Ce trait est donc dans le framework, et voici la méthode showRegistrationForm :

1. public function showRegistrationForm()

2. {

3. return view('auth.register');

4. }

Donc pour changer le nom de la vue il faut surcharger cette méthode dans le contrôleur.

IV-E-1 - La vue register

On va donc chercher cette vue :

Elle a cet aspect :

Cette vue utilise le template resources/views/layouts/app.blade.php. Je ne vais pas entrer dans le détail de tout le code, mais il y a des points intéressants :

Dans le template on trouve ces lignes de code :

1. @if (Auth::guest())

2. <li><a href="{{ URL('/login') }}">Login</a></li>

3. <li><a href="{{ URL('/register') }}">Register</a></li>

4. @else

5. <li class="dropdown">

6. <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">

7. {{ Auth::user()->name }} <span class="caret"></span>

8. </a>

9. ...

10. </li>

11. @endif

On utilise la façade Auth pour vérifier avec la méthode guest qu'on a affaire à un utilisateur non authentifié :

@if (Auth::guest())

En effet dans ce cas on génère les liens pour le login et l'enregistrement. On pourrait tout aussi bien utiliser l'helper auth :

@if (auth()->guest())

On dispose de la méthode auth pour vérifier l'inverse : qu'un utilisateur est authentifié.

On trouve aussi dans le template ce code :

Auth::user()->name

La méthode user permet de disposer d'une instance du modèle pour l'utilisateur authentifié, ici du coup on peut connaître son nom. Ici aussi on pourrait utiliser l'helper :

auth()->user()->name

Comme la vue n'utilise pas le composant laravelcollective/html le jeton (token)pour la protection CSRF est généré avec cet helper :

{{ csrf_field() }}

IV-E-2 - La validation

La validation pour l'enregistrement est présente dans le contrôleur RegisterController :

1. protected function validator(array $data)

2. {

3. return Validator::make($data, [

4. 'name' => 'required|max:255',

5. 'email' => 'required|email|max:255|unique:users',

6. 'password' => 'required|min:6|confirmed',

7. ]);

8. }

De cette manière elle est facile à modifier si vous devez changer des règles ou ajouter un champ.

On peut vérifier que tout fonctionne :

IV-E-3 - La création de l'utilisateur

La création de l'utilisateur se fait dans le contrôleur RegisterController :

1. protected function create(array $data)

2. {

3. return User::create([

4. 'name' => $data['name'],

5. 'email' => $data['email'],

6. 'password' => bcrypt($data['password']),

7. ]);

8. }

Donc là encore il est facile d'apporter des modifications si nécessaire.

Une fois que l'utilisateur est créé dans la base, il est automatiquement connecté et est redirigé sur « home » :

IV-F - La connexion d'un utilisateur

Pour se connecter, un utilisateur doit cliquer sur le lien login de la page d'accueil :

Si vous venez juste d'enregistrer un utilisateur, il faudra le déconnecter avant d'avoir accès au formulaire de login.

Si on clique sur Login, on appelle la méthode showLoginForm du contrôleur LoginController :

Remarquez au passage la déclaration du middleware guest dans ce contrôleur :

1. public function __construct()

2. {

3. $this->middleware('guest', ['except' => 'logout']);

4. }

En effet si on est authentifié c'est qu'on n'a pas besoin de se connecter ! D'autre part avec l'option except on évite d'appliquer le middleware au logout.

Si vous regardez dans ce contrôleur, vous n'allez pas trouver la méthode showLoginForm, par contre vous allez trouver un trait :

use AuthenticatesUsers;

L'espace de noms complet est : Illuminate\Foundation\Auth\AuthenticateUsers. Ce trait est donc dans le framework, et voici la méthode showLoginForm :

1. public function showLoginForm()

2. {

3. return view('auth.login');

4. }

IV-F-1 - La vue login

On va donc chercher cette vue :

Elle a cet aspect :

Cette vue utilise aussi le template resources/views/layouts/app.blade.php. Il n'y a rien de particulier dans cette vue.

IV-F-2 - La validation

La validation pour la connexion est présente dans le trait AuthenticatesUsers :

1. protected function validateLogin(Request $request)

2. {

3. $this->validate($request, [

4. $this->username() => 'required', 'password' => 'required',

5. ]);

6. }

On peut surcharger cette méthode dans le contrôleur pour la modifier. Pour changer seulement le premier élément (username) il y a la méthode dans le trait :

1. public function username()

2. {

3. return 'email';

4. }

Par défaut c'est l'email. Pour changer ça, il faut surcharger cette méthode dans le contrôleur.

On peut vérifier que tout fonctionne :

Il est ensuite effectué une vérification de la présence du couple email/password (credentials) dans la base de données :

IV-F-3 - Se rappeler de moi

Dans le formulaire de connexion il y a une case à cocher « se rappeler de moi » (remember me) :

Si on coche cette case, on reste connecté indéfiniment jusqu'à ce qu'on se déconnecte intentionnellement. Pour que ça fonctionne, il faut une colonne remember_token dans la table users :

Il se trouve que cette colonne est prévue dans la migration de base pour la table.

IV-F-4 - La redirection

Lorsque l'utilisateur est bien authentifié, il est redirigé par la méthode redirectPath du trait Illuminate\Foundation\Auth\RedirectsUsers :

1. public function redirectPath()

2. {

3. return property_exists($this, 'redirectTo') ? $this->redirectTo : '/home';

4. }

Par défaut on redirige sur « home » ou sur la valeur de la propriété redirectTo si elle existe. Donc pour changer la redirection par défaut, il suffit de créer cette propriété avec la valeur voulue.

En fait ce que j'ai écrit ci-dessus est incomplet, il faut préciser quelque chose. On peut arriver sur le formulaire de connexion parce qu'on clique sur le lien correspondant, mais on peut aussi y arriver par l'action du middleware auth. En effet si on veut atteindre une page pour laquelle il faut être authentifié, le middleware auth va nous bloquer et nous rediriger sur le formulaire de connexion. Dans ce cas ce qui serait bien serait, à l'issue de la connexion, d'aller directement sur la page qu'on désirait atteindre au départ.

Sans entrer dans les détails la route désirée est mémorisée dans la session et voici le code pour la redirection :

redirect()->intended($this->redirectPath())

La méthode intended va vérifier si on a mémorisé une route dans la session et l'utiliser si c'est le cas, sinon on utilise la méthode redirectPath qu'on a vue ci-dessus.

IV-G - La déconnexion d'un utilisateur

L'utilisateur dispose d'un lien pour se déconnecter :

Si on clique sur Logout on appelle la méthode logout du contrôleur LoginController :

Remarquez que la méthode est POST, c'est une nouveauté dans cette version 5.3 et plus en cohérence avec le HTTP. Mais du coup un simple lien ne suffit pas. Si vous regardez dans le code du template (resources/views/layouts/app.blade.php) vous y trouvez ces lignes :

1. <a href="{{ url('/logout') }}"

2. onclick="event.preventDefault();

3. document.getElementById('logout-form').submit();">

4. Logout

5. </a>

7. <form id="logout-form" action="{{ url('/logout') }}" method="POST" style="display: none;">

8. {{ csrf_field() }}

9. </form>

On a un formulaire avec la méthode POST et une soumission avec JavaScript.

Si vous regardez dans le contrôleur LoginController vous n'allez pas trouver la méthode logout, par contre vous allez trouver un trait :

use AuthenticatesUsers;

On a déjà vu ce trait plus haut pour le login. Voici la méthode logout dans ce trait :

1. public function logout(Request $request)

2. {

3. $this->guard()->logout();

5. $request->session()->flush();

7. $request->session()->regenerate();

9. return redirect('/');

10. }

La méthode flush efface toutes les informations de la session en cours.

La méthode regenerate modifie l'identifiant de session pour éviter les attaques de fixation de session.

On redirige sur l'URL de base « / ».

IV-H - En résumé

- L'authentification est totalement et simplement prise en charge par Laravel.

- Les migrations pour l'authentification sont déjà dans l'installation de base de Laravel.

- La commande d'Artisan make:auth génère les contrôleurs, les routes et les vues pour l'authentification.

- Les middlewares auth et guest permettent d'interdire les accès aux routes qui concernent uniquement les utilisateurs authentifiés ou non ‌authentifiés.

V - L'oubli du mot de passe

Un mot de passe est plus facile à oublier qu'à retenir, d'autant qu'on en est littéralement submergé ! Toute application avec une authentification doit prévoir ce genre de situation et donner la possibilité aux utilisateurs de se sortir de ce mauvais pas. Le plus simple est de leur permettre de créer un nouveau mot de passe. Laravel est équipé pour cela et nous allons le voir dans ce chapitre.

Le présent chapitre est une suite du précédent et il faut avoir mis en place ce qui concerne l'authentification avec la commande d'Artisan make:auth pour que ça fonctionne.

V-A - La base de données

Avec les migrations réalisées dans le chapitre précédent, il y a la table password_resets dans notre base :

On voit qu'on va mémoriser ici l'adresse email, le jeton (token) et le timestamp (par défaut les jetons sont valables pendant une heure). On trouve ces réglages dans le fichier config/auth.php :

1. 'passwords' => [

2. 'users' => [

3. 'provider' => 'users',

4. 'table' => 'password_resets',

5. 'expire' => 60,

6. ],

7. ],

V-B - Les routes et les contrôleurs

On a quatre routes pour le renouvellement du mot de passe :

Remarquez l'application du middleware guest sur toutes ces routes.

On dispose également de deux contrôleurs :

V-C - Première étape : la demande

Pour l'utilisateur la première étape va consister à cliquer sur le lien « Forgot Your Password? » prévu sur la vue de connexion :

Ce lien correspond à cette route :

On appelle la méthode showLinkRequestForm du contrôleur ForgotPasswordController. Si vous regardez dans ce contrôleur, vous n'allez pas trouver cette méthode, mais un trait :

use SendsPasswordResetEmails;

L'espace de noms complet est Illuminate\Foundation\Auth\SendsPasswordResetEmails. Ce trait est dans le framework et voici la méthode showLinkRequestForm :

1. public function showLinkRequestForm()

2. {

3. return view('auth.passwords.email');

4. }

On utilise la vue auth.passwords.email :

Avec un formulaire qui a cet aspect :

On demande l'adresse email à l'utilisateur. La soumission correspond à cette route :

On appelle la méthode sendResetLinkEmail du contrôleur ForgotPasswordController. Cette fois encore la méthode est dans le trait SendsPasswordResetEmails.

Dans cette méthode il y a la validation :

$this->validate($request, ['email' => 'required|email']);

On peut vérifier que ça fonctionne :

Si la validation passe avec succès, on vérifie que l'adresse existe bien dans la base et on envoie un email à l'utilisateur :

$response = $this->broker()->sendResetLink(
$request->only('email'), $this->resetNotifier()
);

Voici le message reçu :

L'envoi de l'email se fait avec un système de notification dont je vous parlerai dans la troisième partie de ce cours.

Et si tout s'est bien passé, on renvoie le formulaire avec un message :

if ($response === Password::RESET_LINK_SENT) {
return back()->with('status', trans($response));
}

Voici ce que ça donne :

Dans la table password_resets un jeton (token) a été généré :

Si vous regardez le lien qui figure en bas du message vous retrouvez ce jeton comme paramètre, ainsi il sera facile de vérifier la provenance de la demande.

V-D - Deuxième étape : le renouvellement

Quand on appuie sur le bouton du message ou si on clique sur le lien de secours on utilise cette route :

On appelle la méthode showResetForm du contrôleur ResetPasswordController. Si vous regardez dans ce contrôleur vous n'allez encore pas trouver cette méthode, mais un trait :

use ResetsPasswords;

L'espace de noms complet est Illuminate\Foundation\Auth\ResetsPasswords. Ce trait est dans le framework et voici la méthode showResetForm :

1. public function showResetForm(Request $request, $token = null)

2. {

3. return view('auth.passwords.reset')->with(

4. ['token' => $token, 'email' => $request->email]

5. );

6. }

On utilise la vue auth.passwords.reset en lui transmettant le jeton (token) et l'email :

Voici l'aspect du formulaire :

Dans ce formulaire on retrouve le jeton (token) dans un contrôle caché :

<input type="hidden" name="token" value="7ac47bec98262a806b616fb97e9f24ade0204cdf338a0722d6127cc89a928b56">

La soumission de ce formulaire utilise cette route :

On appelle la méthode reset du contrôleur ResetPasswordController. Cette méthode est aussi dans le trait ResetsPasswords.

On commence par une validation :

1. $this->validate($request, [

2. 'token' => 'required', 'email' => 'required|email',

3. 'password' => 'required|confirmed|min:6',

4. ]);

Ensuite on vérifie que l'adresse existe bien, que le jeton est bon et on procède à la mémorisation du nouveau mot de passe :

1. $response = $this->broker()->reset(

2. $this->credentials($request), function ($user, $password) {

3. $this->resetPassword($user, $password);

4. }

5. );

Ensuite si tout va bien on connecte l'utilisateur et on l'envoie sur la page de base du site « / ».

V-E - En résumé

- Un système complet de renouvellement du mot de passe est prévu, avec ses routes, ses contrôleurs et ses vues, avec la commande d'Artisan make:auth‌.

- Le renouvellement se fait en deux étapes : une demande qui reçoit une réponse sous forme d'email sécurisé avec un jeton (token) puis une soumission du nouveau mot de passe avec un formulaire.

VI - La relation 1:n

Pour le moment nous n'avons manipulé qu'une table avec Eloquent. Dans le présent chapitre, nous allons utiliser deux tables et les mettre en relation.

La relation la plus répandue et la plus simple entre deux tables 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.

Comme exemple pour ce chapitre, je vais prendre le cas d'un petit blog personnel avec :

- un affichage des articles ;

- des visiteurs qui pourront consulter les articles ;

- des utilisateurs enregistrés qui pourront aussi rédiger des articles (donc possibilité de se connecter et se déconnecter) ;

- des administrateurs qui pourront aussi supprimer des articles.

Pour ne pas trop alourdir le code, je ne vais pas prévoir la modification des articles.

Partez d'une installation fraîche de Laravel et utilisez la commande d'Artisan que nous avons vue précédemment pour créer les éléments de l'authentification :

php artisan make:auth

Nous allons utiliser cette infrastructure de base pour créer notre exemple.

VI-A - Les migrations

VI-A-1 - Table users

Pour distinguer les rôles nous allons ajouter dans la table users une colonne admin. Dans la migration par défaut ajoutez cette ligne :

1. public function up()

2. {

3. Schema::create('users', function (Blueprint $table) {

4. ...

5. $table->boolean('admin')->default(false);

6. ...

7. });

8. }

On va ainsi créer un champ booléen admin avec comme valeur par défaut false. Ne changez rien au reste du code.

VI-A-2 - Table posts

On va créer une migration pour la table des articles posts :

php artisan make:migration create_posts_table

Complétez le code ainsi :

1. <?php

3. use Illuminate\Support\Facades\Schema;

4. use Illuminate\Database\Schema\Blueprint;

5. use Illuminate\Database\Migrations\Migration;

7. class CreatePostsTable extends Migration

8. {

9. public function up()

10. {

11. Schema::create('posts', function(Blueprint $table) {

12. $table->increments('id');

13. $table->timestamps();

14. $table->string('titre');

15. $table->text('contenu');

16. $table->integer('user_id')->unsigned();

17. $table->foreign('user_id')

18. ->references('id')

19. ->on('users')

20. ->onDelete('restrict')

21. ->onUpdate('restrict');

22. });

23. }

25. public function down()

26. {

27. Schema::table('posts', function(Blueprint $table) {

28. $table->dropForeign('posts_user_id_foreign');

29. });

30. Schema::drop('posts');

31. }

32. }

Normalement vous devez avoir ces trois migrations :

Lancez les migrations :

Vous devez vous retrouver avec les trois tables dans votre base ainsi que la table migrations :

VI-B - La relation

On a la situation suivante :

- un utilisateur peut écrire plusieurs articles ;

- un article est écrit par un seul utilisateur.

Il faut trouver un moyen pour référencer cette relation dans les tables. Le principe est simple : on prévoit dans la table posts une ligne destinée à recevoir l'identifiant de l'utilisateur rédacteur de l'article. On appelle cette ligne une clé étrangère parce qu'on enregistre ici la clé d'une autre table. Voici une représentation visuelle de cette relation :

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 que l'on a créée ci-dessus est destinée aussi à informer la base de cette relation. Regardez ce code :

1. $table->foreign('user_id')

2. ->references('id')

3. ->on('users')

4. ->onDelete('restrict')

5. ->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 avez un utilisateur avec l'id 5 qui a deux articles, donc dans la table posts on a deux enregistrements avec user_id qui a la valeur 5. Si on supprime l'utilisateur que va-t-il se passer ? On risque de se retrouver avec nos deux enregistrements dans la table posts avec une clé étrangère qui ne correspond à aucun enregistrement dans la table users. En mettant restrict, on empêche la suppression d'un utilisateur qui a des articles. On doit donc commencer par supprimer ses articles avant de le supprimer lui-même. On dit que la base assure l'intégrité référentielle. Elle n'acceptera 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 supprimez un utilisateur ça supprimera en cascade les articles de cet utilisateur.

C'est une option qui est rarement utilisée parce qu'elle peut s'avérer dangereuse, surtout dans une base comportant de multiples tables en relation. Mais c'est aussi une stratégie très efficace parce que c'est le moteur de la base de données qui se charge de gérer les enregistrements en relation, vous n'avez ainsi pas à vous en soucier au niveau du code.

On pourrait aussi ne pas signaler à la base qu'il existe une relation et la gérer seulement dans notre code. Mais c'est encore plus dangereux parce que la moindre erreur de gestion des enregistrements dans votre code risque d'avoir des conséquences importantes dans votre base avec de multiples incohérences.

VI-C - Les modèles

Nous avons déjà un modèle User (app/User.php). Il va juste falloir ajouter une méthode pour pouvoir facilement aller trouver les articles d'un utilisateur. Ajoutez ce code dans le modèle User :

1. public function posts()

2. {

3. return $this->hasMany(\App\Post::class);

4. }

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.

Soyez vigilant avec les espaces de noms !

Il nous faut aussi le modèle Post :

php artisan make:model Post

Complétez ainsi le code :

1. <?php

3. namespace App;

5. use Illuminate\Database\Eloquent\Model;

7. class Post extends Model

8. {

9. protected $fillable = [

10. 'titre','contenu','user_id'

11. ];

13. public function user()

14. {

15. return $this->belongsTo(\App\User::class);

16. }

17. }

Ici on a la méthode user (au singulier) qui permet de trouver 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 :

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 en le mettant au pluriel (à la mode anglaise) et en mettant la première lettre en minuscule. Donc avec le modèle Post il en conclut que la table s'appelle posts. Si ce n'était pas satisfaisant il faudrait créer une propriété $table.

Les deux méthodes mises en place permettent de récupérer facilement un enregistrement lié. Par exemple pour avoir tous les articles de l'utilisateur qui a l'id 1 :

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

$user = App\Post::find(1)->user;

Vous voyez que le codage devient limpide avec ces méthodes.

VI-D - Le contrôleur et les routes

VI-D-1 - Le contrôleur

Maintenant que tout est en place au niveau des données, voyons un peu la gestion de tout ça. On va créer un contrôleur de ressource pour les articles qu'on va appeler PostController :

php artisan make:controller PostController -resource

Ce contrôleur devra gérer plusieurs choses :

- la réception de la requête pour afficher les articles du blog et la réponse adaptée ;

- la réception de la requête pour le formulaire pour créer un nouvel article et son envoi ;

- la réception de la soumission du formulaire de création d'un nouvel article (réservé à un utilisateur connecté) et son enregistrement ;

- la réception de la demande de suppression d'un article (réservé à un administrateur) et sa suppression.

Pour simplifier, je ne vais pas prévoir la possibilité de modifier un article.

Voici le code modifié du contrôleur :

1. <?php

3. namespace App\Http\Controllers;

5. use App\Repositories\PostRepository;

6. use App\Http\Requests\PostRequest;

7. use App\Post;

9. class PostController extends Controller

10. {

11. protected $postRepository;

13. protected $nbrPerPage = 4;

15. public function __construct(PostRepository $postRepository)

16. {

17. $this->middleware('auth')->except('index');

18. $this->middleware('admin')->only('destroy');

20. $this->postRepository = $postRepository;

21. }

23. public function index()

24. {

25. $posts = $this->postRepository->getPaginate($this->nbrPerPage);

27. return view('posts.liste', compact('posts'));

28. }

30. public function create()

31. {

32. return view('posts.create');

33. }

35. public function store(PostRequest $request)

36. {

37. $inputs = array_merge($request->all(), ['user_id' => $request->user()->id]);

39. $this->postRepository->store($inputs);

41. return redirect()->route('post.index');

42. }

44. public function destroy(Post $post)

45. {

46. $this->postRepository->destroy($post);

48. return back();

49. }

50. }

Comme à l'accoutumée j'injecte la requête de formulaire et le repository.

Notez l'utilisation des middlewares pour filtrer les utilisateurs, nous allons voir cela un peu plus loin.

VI-D-2 - Les routes

On a vu dans le chapitre sur les ressources comment créer les routes de ce genre de contrôleur. Il va juste falloir indiquer qu'on ne veut pas utiliser les sept méthodes disponibles, mais juste certaines. On va aussi établir une redirection de la route de base « / » vers la ressource :

1. Route::get('/', function () {

2. return redirect()->route('post.index');

3. });

5. Auth::routes();

7. Route::resource('post', 'PostController', ['except' => ['show', 'edit', 'update']]);

Pour mémoire Auth::routes(); a été généré par Artisan pour créer les routes de l'authentification.

Vous avez ainsi toutes ces routes :

VI-E - Le repository

Pour la gestion on va placer les fichiers dans le dossier app/Repositories comme nous l'avons déjà fait pour la gestion des utilisateurs :

Avec ce code :

1. <?php

3. namespace App\Repositories;

5. use App\Post;

7. class PostRepository

8. {

9. protected $post;

11. public function __construct(Post $post)

12. {

13. $this->post = $post;

14. }

16. public function getPaginate($n)

17. {

18. return $this->post->with('user')

19. ->orderBy('posts.created_at', 'desc')

20. ->paginate($n);

21. }

23. public function store($inputs)

24. {

25. $this->post->create($inputs);

26. }

28. public function destroy(Post $post)

29. {

30. $post->delete();

31. }

32. }

Nous allons voir plus loin l'utilité de toutes ces méthodes.

VI-F - Les middlewares

On a vu que dans le contrôleur on applique deux middlewares :

- auth : accès réservé aux utilisateurs authentifiés à part pour la méthode index pour afficher le blog,

- admin : accès réservé aux administrateurs pour la méthode destroy.

On a déjà rencontré le premier middleware, il est déjà prévu dans Laravel, par contre le second n'existe pas.

Il faut donc le créer. Encore une fois nous allons utiliser Artisan :

php artisan make:middleware Admin

On trouve le fichier bien rangé :

Modifiez ainsi le code :

1. <?php

3. namespace App\Http\Middleware;

5. use Closure;

7. class Admin

8. {

9. /**

10. * Handle an incoming request.

11. *

12. * @param  \Illuminate\Http\Request  $request

13. * @param  \Closure  $next

14. * @return mixed

15. */

16. public function handle($request, Closure $next)

17. {

18. if ($request->user()->admin) {

19. return $next($request);

20. }

22. return redirect('post');

23. }

24. }

Si l'utilisateur n'est pas un administrateur, on redirige sur l'affichage du blog. Remarquez qu'on ne vérifie pas à ce niveau qu'on a un utilisateur authentifié parce que dans le constructeur du contrôleur le filtre auth est placé avant le filtre admin. Si c'était 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 ça ne suffit pas, il faut maintenant un lien entre son nom et la classe qu'on vient de créer.

Regardez dans le fichier app/Http/Kernel.php ces lignes de code :

1. protected $routeMiddleware = [

2. 'auth' => \Illuminate\Auth\Middleware\Authenticate::class,

3. 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,

4. 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,

5. 'can' => \Illuminate\Auth\Middleware\Authorize::class,

6. 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,

7. 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,

8. ];

Vous trouvez ici tous les middlewares déclarés, il suffit d'ajouter le nouveau :

1. protected $routeMiddleware = [

2. ...

3. 'admin' => \App\Http\Middleware\Admin::class,

4. ];

VI-G - La validation

Voyons maintenant la validation. On va encore créer une requête de formulaire :

php artisan make:request PostRequest

Elle se place dans le dossier qui se crée pour l'occasion :

Et on complète ainsi le code :

1. <?php

3. namespace App\Http\Requests;

5. use Illuminate\Foundation\Http\FormRequest;

7. class PostRequest extends FormRequest

8. {

9. /**

10. * Determine if the user is authorized to make this request.

11. *

12. * @return bool

13. */

14. public function authorize()

15. {

16. return true;

17. }

19. /**

20. * Get the validation rules that apply to the request.

21. *

22. * @return array

23. */

24. public function rules()

25. {

26. return [

27. 'titre' => 'bail|required|max:255',

28. 'contenu' => 'required'

29. ];

30. }

31. }

VI-H - La population

VI-H-1 - Les fabriques (model factories)

Nous allons voir maintenant comment remplir nos tables avec des enregistrements pour faire nos essais. Laravel nous permet de définir un tableau d'attributs pour les modèles avec les fabriques (model factories). Regardez le fichier database/factories/ModelFactory.php :

1. <?php

3. /*

4. |--------------------------------------------------------------------------

5. | Model Factories

6. |--------------------------------------------------------------------------

7. |

8. | Here you may define all of your model factories. Model factories give

9. | you a convenient way to create models for testing and seeding your

10. | database. Just tell the factory how a default model should look.

11. |

12. */

14. $factory->define(App\User::class, function (Faker\Generator $faker) {

15. static $password;

17. return [

18. 'name' => $faker->name,

19. 'email' => $faker->safeEmail,

20. 'password' => $password ?: $password = bcrypt('secret'),

21. 'remember_token' => str_random(10),

22. ];

23. });

Faker est un générateur de données virtuelles aléatoires qui sait pratiquement tout faire.

Comme on a ajouté la colonne admin on va la prévoir dans la fabrique. D'autre part on va utiliser un mot de passe systématique pour nous simplifier la vie :

1. $factory->define(App\User::class, function (Faker\Generator $faker) {

2. return [

3. 'name' => $faker->name,

4. 'email' => $faker->safeEmail,

5. 'password' => bcrypt('secret'),

6. 'admin' => $faker->boolean,

7. 'remember_token' => str_random(10),

8. ];

9. });

On va aussi prévoir une fabrique pour les articles dans le même fichier :

1. $factory->define(App\Post::class, function (Faker\Generator $faker) {

2. return [

3. 'titre' => $faker->sentence(2),

4. 'contenu' => $faker->paragraph(rand(8, 15)),

5. 'created_at' => $faker->dateTimeThisYear(),

6. ];

7. });

la documentation détaillée.

VI-H-2 - La population

Regardez le fichier database/seeds/DatabaseSeeder :

Par défaut il comporte ce code :

1. <?php

3. use Illuminate\Database\Seeder;

5. class DatabaseSeeder extends Seeder

6. {

7. /**

8. * Run the database seeds.

9. *

10. * @return void

11. */

12. public function run()

13. {

14. // $this->call(UsersTableSeeder::class);

15. }

16. }

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. Comme on n'a que deux tables à remplir, on va mettre tout le code dans ce fichier :

1. public function run()

2. {

3. factory(App\User::class, 5)

4. ->create()

5. ->each(function ($user) {

6. $user->posts()->saveMany(factory(App\Post::class, rand(2, 5))->make());

7. });

8. }

On appelle la fabrique (factory) pour le modèle des utilisateurs (App\User::class), on en veut 5, on les crée (create) pour chacun des utilisateurs créés (each $user) on utilise la relation (posts()) pour créer plusieurs (saveMany) articles (App\Post::class) en utilisant la fabrique (factory), on en veut entre 2 et 5 (rand(2, 5)).

Il ne vous reste plus qu'à lancer la population :

php artisan db:seed

Vous aurez dans la base cinq utilisateurs :

Et des articles (table posts) affectés aux utilisateurs :

On a des dates aléatoires pour la colonne created_at, ce qui va nous permettre de les trier.

VI-I - Fonctionnement

VI-I-1 - La liste des articles

La liste des articles est obtenue avec l'URL (méthode get) : …/post

Elle arrive sur la méthode index du contrôleur :

1. public function index()

2. {

3. $posts = $this->postRepository->getPaginate($this->nbrPerPage);

5. return view('posts.liste', compact('posts'));

6. }

Ici on envoie le nombre d'articles par page (placé dans la propriété $nbrPerPage) à la méthode getPaginate du repository :

1. public function getPaginate($n)

2. {

3. return $this->post->with('user')

4. ->orderBy('created_at', 'desc')

5. ->paginate($n);

6. }

On veut les articles avec (with) l'utilisateur (user), dans l'ordre des dates de création (created_at) descendant (desc) avec une pagination de n articles ($n).

Il existe la méthode latest (et oldest pour l'inverse) qui permet de simplifier la syntaxe :

return $this->post->with('user')
->latest()
->paginate($n);

VI-I-2 - L'ajout d'un article

La demande du formulaire de création d'un article se fait avec l'URL (méthode get) : …/post/create

Le contrôleur renvoie directement la vue :

1. public function create()

2. {

3. return view('posts.create');

4. }

Le retour du formulaire se fait avec l'URL (méthode post) : …/post

Remarquez que cette méthode est protégée par le middleware auth :

$this->middleware('auth')->except('index');

On arrive sur la méthode store du contrôleur :

1. public function store(PostRequest $request)

2. {

3. $inputs = array_merge($request->all(), ['user_id' => $request->user()->id]);

5. $this->postRepository->store($inputs);

7. return redirect()->route('post.index');

8. }

On injecte la requête de formulaire pour la validation, je n'insiste pas parce qu'il n'y a rien de nouveau à ce niveau. On récupère les entrées du formulaire pour le titre et le contenu. Pour l'identifiant de l'utilisateur, on sait qu'il est forcément connecté alors on récupère cet identifiant avec la requête. Si la validation se passe bien on envoie à la méthode store du repository :

1. public function store($inputs)

2. {

3. $this->post->create($inputs);

4. }

L'assignement de masse fonctionne parce qu'on a prévu la propriété $fillable dans le modèle Post :

protected $fillable = [
'titre','contenu','user_id'
];

VI-I-3 - Suppression d'un article

Enfin on supprime un article avec l'URL (méthode delete) : …/post/id

Où id représente l'identifiant de l'article à supprimer. On tombe sur la méthode destroy du contrôleur :

1. public function destroy(Post $post)

2. {

3. $this->postRepository->destroy($post);

5. return back();

6. }

Remarquez que cette méthode est protégée par le middleware admin :

$this->middleware('admin')->only('destroy');

Avec la liaison implicite on a directement le modèle de l'article correspondant dans la variable $post. On l'envoie à la méthode destroy du repository :

1. public function destroy(Post $post)

2. {

3. $post->delete();

4. }

Là on supprime l'article avec la méthode delete du modèle.

VI-J - Le template

Voyons à présent les vues. On dispose déjà d'un template (resources/views/layouts/app.blade.php) avec l'installation de l'authentification :

Alors on va l'utiliser pour avoir une cohérence visuelle de l'application. On va juste lui apporter des petites modifications (changement du titre, francisation, ajout d'un item dans la barre de navigation) , voici le code résultant :

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

4. <meta charset="utf-8">

5. <meta http-equiv="X-UA-Compatible" content="IE=edge">

6. <meta name="viewport" content="width=device-width, initial-scale=1">

8. <!-- CSRF Token -->

9. <meta name="csrf-token" content="{{ csrf_token() }}">

11. <title>Mon joli blog</title>

13. <!-- Styles -->

14. <link href="/css/app.css" rel="stylesheet">

16. <!-- Scripts -->

17. <script>

18. window.Laravel = <?php echo json_encode([

19. 'csrfToken' => csrf_token(),

20. ]); ?>

21. </script>

22. </head>

23. <body>

24. <nav class="navbar navbar-default navbar-static-top">

25. <div class="container">

26. <div class="navbar-header">

28. <!-- Collapsed Hamburger -->

29. <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#app-navbar-collapse">

30. <span class="sr-only">Toggle Navigation</span>

31. <span class="icon-bar"></span>

32. <span class="icon-bar"></span>

33. <span class="icon-bar"></span>

34. </button>

36. <!-- Branding Image -->

37. <a class="navbar-brand" href="{{ URL('/') }}">

38. Mon joli Blog

39. </a>

40. </div>

42. <div class="collapse navbar-collapse" id="app-navbar-collapse">

43. <!-- Left Side Of Navbar -->

44. <ul class="nav navbar-nav">

46. </ul>

48. <!-- Right Side Of Navbar -->

49. <ul class="nav navbar-nav navbar-right">

50. <!-- Authentication Links -->

51. @if (Auth::guest())

52. <li><a href="{{ URL('/login') }}">Se connecter</a></li>

53. <li><a href="{{ URL('/register') }}">S'enregistrer</a></li>

54. @else

55. <li><a href="{{ URL('/post/create') }}">Créer un article</a></li>

56. <li class="dropdown">

57. <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">

58. {{ Auth::user()->name }} <span class="caret"></span>

59. </a>

61. <ul class="dropdown-menu" role="menu">

62. <li>

63. <a href="{{ url('/logout') }}"

64. onclick="event.preventDefault();

65. document.getElementById('logout-form').submit();">

66. Logout

67. </a>

69. <form id="logout-form" action="{{ url('/logout') }}" method="POST" style="display: none;">

70. {{ csrf_field() }}

71. </form>

72. </li>

73. </ul>

74. </li>

75. @endif

76. </ul>

77. </div>

78. </div>

79. </nav>

81. @yield('content')

83. <!-- Scripts -->

84. <script src="/js/app.js"></script>

85. </body>

86. </html>

VI-K - L'affichage des articles

Nous avons besoin d'une vue pour afficher les articles du blog (resources/views/posts/liste.blade.php) :

Avec ce code :

1. @extends('layouts.app')

3. @section('content')

4. <div class="container">

5. @if(isset($info))

6. <div class="row alert alert-info">{{ $info }}</div>

7. @endif

8. {!! $posts->links() !!}

9. @foreach($posts as $post)

10. <article class="row bg-primary">

11. <div class="col-md-12">

12. <header>

13. <h1>{{ $post->titre }}</h1>

14. </header>

15. <hr>

16. <section>

17. <p>{{ $post->contenu }}</p>

18. @if(auth()->check() and auth()->user()->admin)

19. <form method="POST" action="{{ route('post.destroy', ['id' => $post->id]) }}">

20. {{ method_field('DELETE') }}

21. {{ csrf_field() }}

22. <input class="btn btn-danger btn-xs" onclick="return confirm('Vraiment supprimer cet article ?')" type="submit" value="Supprimer cet article">

23. </form>

24. @endif

25. <em class="pull-right">

26. {{ $post->user->name }} le {!! $post->created_at->format('d-m-Y') !!}

27. </em>

28. </section>

29. </div>

30. </article>

31. <br>

32. @endforeach

33. {!! $posts->links() !!}

34. </div>

35. @endsection

Avec cet aspect pour un utilisateur non connecté :

Le lien « Se connecter » ouvre le formulaire de connexion comme on l'a vu dans le chapitre sur l'authentification.

Pour avoir une redirection correcte après la connexion, il faut bien renseigner la propriété $redirectTo dans le contrôleur LoginController :

protected $redirectTo = 'post';

La génération des articles dans la vue se fait avec un foreach :

@foreach($posts as $post)
...
@endforeach

Si vous vous connectez, vous retournez au blog avec deux liens supplémentaires :

On utilise une condition pour adapter les liens :

1. @if (Auth::guest())

2. <li><a href="{{ URL('/login') }}">Se connecter</a></li>

3. <li><a href="{{ URL('/register') }}">S'enregistrer</a></li>

4. @else

5. <li><a href="{{ URL('/post/create') }}">Créer un article</a></li>

6. <li class="dropdown">

7. ...

8. Logout

9. ...

10. </li>

11. @endif

La méthode guest permet de savoir si un utilisateur est un simple visiteur non authentifié. C'est le contraire de la méthode check qu'on pourrait utiliser en inversant la logique.

Si un administrateur est connecté, il dispose en plus de boutons pour supprimer les articles :

On utilise encore une condition pour détecter un administrateur :

@if(auth()->check() and auth()->user()->admin)
...
@endif

Il faut que l'utilisateur soit connecté (check) et que ce soit un administrateur (auth()->user()->admin).

Comme on n'utilise pas laravelcollective/html dans cet exemple (c'est bien aussi de voir comment faire sans ce composant) il faut construire les formulaires avec tout le code nécessaire.

Pour la destruction des articles, on doit aboutir sur cette route :

Il nous faut une méthode DELETE, une méthode qui n'est pas supportée dans les formulaires (tout comme PUT et PATCH). Alors on déclare une méthode POST comme action, mais on ajoute un contrôle caché _method avec la valeur DELETE. À l'arrivée la requête est interprétée comme une requête DELETE. Regardez le code de ce formulaire :

1. <form method="POST" action="{{ route('post.destroy', ['id' => $post->id]) }}">

2. {{ method_field('DELETE') }}

3. {{ csrf_field() }}

4. <input class="btn btn-danger btn-xs" onclick="return confirm('Vraiment supprimer cet article ?')" type="submit" value="Supprimer cet article">

5. </form>

On utilise l'helper method_field pour générer ce code :

<input type="hidden" name="_method" value="DELETE">

J'ai aussi prévu une simple confirmation en JavaScript avant de supprimer effectivement l'article.

VI-L - La création d'un article

Voici maintenant la vue pour le formulaire de création d'un article (resources/views/posts/create.blade.php) :

Avec ce code :

1. @extends('layouts.app')

3. @section('content')

4. <div class="col-sm-offset-3 col-sm-6">

5. <div class="panel panel-default">

6. <div class="panel-heading">Ajout d'un article</div>

7. <div class="panel-body">

8. <form method="POST" action="{{ URL('/post') }}">

9. {{ csrf_field() }}

10. <div class="form-group{{ $errors->has('titre') ? ' has-error' : '' }}">

11. <input class="form-control" placeholder="Titre" name="titre" type="text" value="{{ old('titre') }}" autofocus>

12. @if ($errors->has('titre'))

13. <span class="help-block">

14. <strong>{{ $errors->first('titre') }}</strong>

15. </span>

16. @endif

17. </div>

18. <div class="form-group{{ $errors->has('contenu') ? ' has-error' : '' }}">

19. <textarea class="form-control" placeholder="Contenu" name="contenu" cols="50" rows="10">{{ old('contenu') }}</textarea>

20. @if ($errors->has('contenu'))

21. <span class="help-block">

22. <strong>{{ $errors->first('contenu') }}</strong>

23. </span>

24. @endif

25. </div>

26. <button type="submit" class="btn btn-primary pull-right">Envoyer !</button>

27. </form>

29. </div>

30. </div>

31. </div>

32. @endsection

Voici l'apparence du formulaire :

Là encore le fait de ne pas utiliser le composant laravellollective/html oblige à :

- écrire tout le code ;

- prévoir de remplir les contrôles avec les anciennes valeurs saisies en cas d'erreur dans la validation.

Cette dernière action se fait en utilisant l'helper old. Par exemple pour le titre on a :

<input class="form-control" placeholder="Titre" name="titre" type="text" value="{{ old('titre') }}" autofocus>

Imaginez que vous ayez ‌saisi le titre, mais pas le contenu, au retour de la validation vous allez retrouver votre titre :

C'est exactement pareil pour le contenu.

VI-M - 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.

VII - La relation n:n (1/2)

Dans le précédent chapitre, nous avons vu la relation de type 1:n, la plus simple et la plus répandue. Nous allons maintenant étudier la relation de type n:n, plus délicate à comprendre et à mettre en œuvre. Nous allons voir qu'Eloquent permet de simplifier la gestion de ce type de relation.

Je vais poursuivre l'exemple du blog personnel débuté au chapitre précédent avec la possibilité d'ajouter des mots-clés (tags) aux articles.

VII-A - Les données

VII-A-1 - La relation n:n

Imaginez une relation entre deux tables A et B qui permet de dire :

- je peux avoir une ligne de la table A en relation avec plusieurs lignes de la table B ;

- je peux avoir une ligne de la table B en relation avec plusieurs lignes de la table A.

Cette relation ne se résout pas comme nous l'avons vu au chapitre précédent avec une simple clé étrangère dans une des tables. En effet il nous faudrait des clés dans les deux tables et plusieurs clés, ce qui n'est pas possible à réaliser.

La solution consiste à créer une table intermédiaire (nommée table pivot) qui sert à mémoriser les clés étrangères.

Voici un schéma de ce que nous allons réaliser :

La table pivot contient les clés des deux tables :

- post_id pour mémoriser la clé de la table posts,

- tag_id pour mémoriser la clé de la table tags.

De cette façon on peut avoir plusieurs enregistrements liés entre les deux tables, il suffit à chaque fois d'enregistrer les deux clés dans la table pivot. Évidemment au niveau du code ça demande un peu d'intendance parce qu'il y a une table supplémentaire à gérer.

Par convention le nom de la table pivot est composé des deux noms des tables au singulier pris dans l'ordre alphabétique.

VII-A-2 - Les migrations

Nous allons continuer à utiliser les tables users et posts que nous avons vues aux chapitres précédents. Nous allons créer une nouvelle table tags destinée à mémoriser les mots-clés. Commencez par supprimer toutes les tables de votre base de données, sinon vous risquez d'avoir des conflits avec les enregistrements que nous allons créer.

Supprimez aussi la table migrations.

Normalement vous devez déjà disposer des migrations pour les tables users, password_resets et posts. Nous allons ajouter les deux tables : tags et post_tag.

Créez une nouvelle migration pour la table tags :

php artisan make:migration create_tags_table

Modifiez ainsi le code :

1. <?php

3. use Illuminate\Support\Facades\Schema;

4. use Illuminate\Database\Schema\Blueprint;

5. use Illuminate\Database\Migrations\Migration;

7. class CreateTagsTable extends Migration

8. {

9. /**

10. * Run the migrations.

11. *

12. * @return void

13. */

14. public function up()

15. {

16. Schema::create('tags', function(Blueprint $table) {

17. $table->increments('id');

18. $table->timestamps();

19. $table->string('tag', 50)->unique();

20. $table->string('tag_url', 60)->unique();

21. });

22. }

24. /**

25. * Reverse the migrations.

26. *

27. * @return void

28. */

29. public function down()

30. {

31. Schema::drop('tags');

32. }

33. }

On prévoit les champs :

- id : clé unique incrémentée ;

- created_at et updated_at créées par timestamps ;

- tag : le mot clé unique limité à 50 caractères ;

- tag_url : la version du tag à inclure dans l'URL (avec 60 comme limite pour couvrir les cas les plus défavorables).

Il nous faut deux champs pour le tag, en effet il va falloir qu'on le transmette dans l'URL pour la recherche par tag, or l'utilisateur risque de rentrer des accents par exemple (ou pire des « / »), nous allons convertir ces caractères spéciaux en caractères adaptés aux URL.

Créez une nouvelle migration pour la table post_tag :

php artisan make:migration create_post_tag_table

Complétez ainsi le code :

1. <?php

3. use Illuminate\Support\Facades\Schema;

4. use Illuminate\Database\Schema\Blueprint;

5. use Illuminate\Database\Migrations\Migration;

7. class CreatePostTagTable extends Migration

8. {

9. /**

10. * Run the migrations.

11. *

12. * @return void

13. */

14. public function up()

15. {

16. Schema::create('post_tag', function(Blueprint $table) {

17. $table->increments('id');

18. $table->integer('post_id')->unsigned();

19. $table->integer('tag_id')->unsigned();

20. $table->foreign('post_id')->references('id')->on('posts')

21. ->onDelete('restrict')

22. ->onUpdate('restrict');

24. $table->foreign('tag_id')->references('id')->on('tags')

25. ->onDelete('restrict')

26. ->onUpdate('restrict');

27. });

28. }

30. /**

31. * Reverse the migrations.

32. *

33. * @return void

34. */

35. public function down()

36. {

37. Schema::table('post_tag', function(Blueprint $table) {

38. $table->dropForeign('post_tag_post_id_foreign');

39. $table->dropForeign('post_tag_tag_id_foreign');

40. });

42. Schema::drop('post_tag');

43. }

44. }

On prévoit les champs :

- post_id : clé étrangère pour la table posts ;

- tag_id : clé étrangère pour la table tags.

J'ai encore prévu l'option restrict pour les cascades pour sécuriser les opérations sur la base.

Normalement vous devez avoir ces migrations :

Lancez les migrations :

Vous devez ainsi vous retrouver avec ces six tables dans votre base :

Pour ne pas recevoir d'erreur, il faut que les migrations se fassent dans le bon ordre ! L'ordre des migrations est donné par leur date de création, il suffit donc de changer la date dans le nom des migrations pour en changer l'ordre.

VII-B - Les modèles

On va avoir besoin de déclarer la relation n:n dans le modèle Post :

1. <?php

3. namespace App;

5. use Illuminate\Database\Eloquent\Model;

7. class Post extends Model

8. {

9. protected $fillable = [

10. 'titre','contenu','user_id'

11. ];

13. public function user()

14. {

15. return $this->belongsTo(\App\User::class);

16. }

18. public function tags()

19. {

20. return $this->belongsToMany(\App\Tag::class);

21. }

22. }

La méthode tags permet de récupérer les tags qui sont en relation avec l'article. On utilise la méthode belongsToMany d'Eloquent pour le faire.

On va aussi avoir besoin d'un modèle pour les tags :

php artisan make:model Tag

Complétez ainsi le code :

1. <?php

3. namespace App;

5. use Illuminate\Database\Eloquent\Model;

7. class Tag extends Model

8. {

9. protected $fillable = [

10. 'tag','tag_url'

11. ];

13. public function posts()

14. {

15. return $this->belongsToMany('App\Post');

16. }

17. }

On a la méthode réciproque de la précédente : posts permet de récupérer les articles en relation avec le tag.

Voici une schématisation de cette relation avec les deux méthodes symétriques :

On se retrouve avec ces trois modèles :

On pourrait créer un dossier pour les ranger, mais comme il y en a peu on va les garder comme cela. Si on le faisait, il faudrait adapter les espaces de noms en conséquence. Il faudrait surtout bien renseigner l'espace de noms dans le fichier config/auth.php pour le modèle User.

VII-C - La population

Dans le précédent chapitre on a créé les fabriques (model factories) pour les modèles User et Post. On va ajouter (fichier database/factories/ModelFactory.php) celle pour le modèle Tag :

1. $factory->define(App\Tag::class, function (Faker\Generator $faker) {

2. $tag = $faker->unique()->word();

3. return [

4. 'tag' => $tag,

5. 'tag_url' => $tag,

6. ];

7. });

On va se contenter d'un simple mot.

Faker nous offre la méthode unique pour nous assurer de l'unicité du mot choisi.

On va aussi modifier le fichier pour la population (database/seeds/DatabaseSeeder.php) :

1. <?php

3. use Illuminate\Database\Seeder;

5. class DatabaseSeeder extends Seeder

6. {

7. /**

8. * Run the database seeds.

9. *

10. * @return void

11. */

12. public function run()

13. {

14. factory(App\User::class, 5)

15. ->create()

16. ->each(function ($user) {

17. $user->posts()->saveMany(factory(App\Post::class, rand(2, 5))->make());

18. }

19. );

21. factory(App\Tag::class, 10)->create();

23. $posts = App\Post::all();

25. foreach ($posts as $post) {

26. $numbers = range(1, 10);

27. shuffle($numbers);

28. $n = rand(2, 4);

29. for($i = 0; $i < $n; ++$i) {

30. $post->tags()->attach($numbers[$i]);

31. }

32. }

33. }

34. }

On crée dix tags et ensuite de façon aléatoire on en affecte aux articles.

Il ne reste plus qu'à lancer la population :

php artisan db:seed

Vérifiez dans votre base de données que vous avez des informations correctes.

VII-D - La validation

Nous allons avoir un cas de validation un peu particulier. En effet comme je l'ai dit ci-dessus les tags vont être entrés dans un contrôle de texte, séparés par des virgules. On a prévu dans la table tags qu'ils ne devraient pas dépasser 50 caractères.

On ne dispose pas dans l'arsenal des règles de validation de Laravel d'une telle possibilité, il va donc falloir la créer.

On a déjà créé une classe PostRequest dans le chapitre précédent, il faut ajouter la règle pour les tags :

1. <?php

3. namespace App\Http\Requests;

5. use Illuminate\Foundation\Http\FormRequest;

7. class PostRequest extends FormRequest

8. {

9. /**

10. * Determine if the user is authorized to make this request.

11. *

12. * @return bool

13. */

14. public function authorize()

15. {

16. return true;

17. }

19. /**

20. * Get the validation rules that apply to the request.

21. *

22. * @return array

23. */

24. public function rules()

25. {

26. return [

27. 'titre' => 'bail|required|max:255',

28. 'contenu' => 'required',

29. 'tags' => ['Regex:/^[A-Za-z0-9-éèàù]{1,50}?(,[A-Za-z0-9-éèàù]{1,50})*$/'],

30. ];

31. }

32. }

Comme le cas est particulier, j'ai utilisé une expression rationnelle. Il ne reste plus qu'à traiter le message.

Si vous regardez dans le fichier resources/lang/en/validation.php vous trouvez ce code :

1. 'custom' => [

2. 'attribute-name' => [

3. 'rule-name' => 'custom-message',

4. ],

5. ],

C'est ici qu'on peut ajouter des messages spécifiques. On va donc écrire :

1. 'custom' => [

2. 'tags' => [

3. 'regex' => "tags, separated by commas (no spaces), should have a maximum of 50 characters.",

4. ],

5. ],

On va faire la même chose dans le fichier du français (puisqu'on a localisé en français) :

1. 'custom' => [

2. 'tags' => [

3. 'regex' => "Les mots-clés, séparés par des virgules (sans espaces), doivent avoir au maximum 50 caractères alphanumériques.",

4. ],

5. ],

VII-E - Le contrôleur et les routes

VII-E-1 - Le contrôleur

Maintenant que tout est en place au niveau des données et de la validation, voyons un peu la gestion de tout ça. On a déjà un contrôleur PostController, mais on doit le compléter pour le fonctionnement avec les tags :

1. <?php

3. namespace App\Http\Controllers;

5. use App\Repositories\PostRepository;

6. use App\Repositories\TagRepository;

7. use App\Http\Requests\PostRequest;

8. use App\Post;

10. class PostController extends Controller

11. {

12. protected $postRepository;

14. protected $nbrPerPage = 4;

16. public function __construct(PostRepository $postRepository)

17. {

18. $this->middleware('auth')->except('index', 'indexTag');

19. $this->middleware('admin')->only('destroy');

21. $this->postRepository = $postRepository;

22. }

24. public function index()

25. {

26. $posts = $this->postRepository->getWithUserAndTagsPaginate($this->nbrPerPage);

28. return view('posts.liste', compact('posts'));

29. }

31. public function create()

32. {

33. return view('posts.create');

34. }

36. public function store(PostRequest $request, TagRepository $tagRepository)

37. {

38. $inputs = array_merge($request->all(), ['user_id' => $request->user()->id]);

40. $post = $this->postRepository->store($inputs);

42. if(isset($inputs['tags'])) {

43. $tagRepository->store($post, $inputs['tags']);

44. }

46. return redirect(route('post.index'));

47. }

49. public function destroy(Post $post)

50. {

51. $this->postRepository->destroy($post);

53. return back();

54. }

56. public function indexTag($tag)

57. {

58. $posts = $this->postRepository->getWithUserAndTagsForTagPaginate($tag, $this->nbrPerPage);

60. return view('posts.liste', compact('posts'))

61. ->with('info', 'Résultats pour la recherche du mot-clé : ' . $tag);

62. }

63. }

J'ai ajouté la méthode indexTag qui doit lancer la recherche des articles qui comportent ce tag et envoyer les informations dans la vue liste. J'ai aussi un peu remanié le code.

VII-E-2 - Les routes

Il faut ajouter la route pour aboutir sur cette nouvelle méthode :

Route::resource('post', 'PostController', ['except' => ['show', 'edit', 'update']]);
Route::get('post/tag/{tag}', 'PostController@indexTag');

Vous devez donc avoir toutes ces routes :

VII-F - Les repositories

VII-F-1 - Le repository pour les articles

Voici le repository pour les articles (app/Repositories/PostRepository.php), modifié pour tenir compte des tags :

1. <?php

3. namespace App\Repositories;

5. use App\Post;

7. class PostRepository

8. {

9. protected $post;

11. public function __construct(Post $post)

12. {

13. $this->post = $post;

14. }

16. protected function queryWithUserAndTags()

17. {

18. return $this->post->with('user', 'tags')->latest();

19. }

21. public function getWithUserAndTagsPaginate($n)

22. {

23. return $this->queryWithUserAndTags()->paginate($n);

24. }

26. public function getWithUserAndTagsForTagPaginate($tag, $n)

27. {

28. return $this->queryWithUserAndTags()

29. ->whereHas('tags', function($query) use ($tag) {

30. $query->where('tags.tag_url', $tag);

31. })->paginate($n);

32. }

34. public function store($inputs)

35. {

36. return $this->post->create($inputs);

37. }

39. public function destroy(Post $post)

40. {

41. $post->tags()->detach();

42. $post->delete();

43. }

44. }

Vous êtes peut-être surpris par la longueur de certains des noms de fonctions. C'est un choix syntaxique. Je préfère des noms explicites, quitte à les allonger.

VII-F-2 - Le repositoy pour les tags

Et voici le repository pour les tags (app/Repositories/TagRepository.php) :

1. <?php

2. namespace App\Repositories;

4. use App\Tag;

5. use Illuminate\Support\Str;

7. class TagRepository

8. {

9. protected $tag;

11. public function __construct(Tag $tag)

12. {

13. $this->tag = $tag;

14. }

16. public function store($post, $tags)

17. {

18. $tags = explode(',', $tags);

20. foreach ($tags as $tag) {

22. $tag = trim($tag);

24. $tag_url = Str::slug($tag);

26. $tag_ref = $this->tag->where('tag_url', $tag_url)->first();

28. if(is_null($tag_ref))

29. {

30. $tag_ref = new $this->tag([

31. 'tag' => $tag,

32. 'tag_url' => $tag_url

33. ]);

35. $post->tags()->save($tag_ref);

37. } else {

39. $post->tags()->attach($tag_ref->id);

41. }

42. }

43. }

44. }

Dans le prochain chapitre, nous verrons comment tout ça fonctionne ainsi que les vues.

VII-G - En résumé

- Une relation de type n:n nécessite la création d'une table pivot.

- Eloquent gère élégamment les tables pivots avec des méthodes adaptées.‌

- On peut créer des règles de validation personnalisée avec une expression rationnelle.

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 :

1. public function getWithUserAndTagsPaginate($n)

2. {

3. return $this->queryWithUserAndTags()->paginate($n);

4. }

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

1. protected function queryWithUserAndTags()

2. {

3. return $this->post->with('user', 'tags')->latest();

4. }

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

1. select count(*) as aggregate from `posts`

3. select * from `posts` order by `created_at` desc limit 4 offset 0

5. select * from `users` where `users`.`id` in ('3', '2', '4', '5')

7. 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')

9. 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 :

1. public function store($inputs)

2. {

3. return $this->post->create($inputs);

4. }

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

1. <?php

2. public function store($post, $tags)

3. {

4. $tags = explode(',', $tags);

6. foreach ($tags as $tag) {

8. $tag = trim($tag);

10. $tag_url = Str::slug($tag);

12. $tag_ref = $this->tag->where('tag_url', $tag_url)->first();

14. if(is_null($tag_ref))

15. {

16. $tag_ref = new $this->tag([

17. 'tag' => $tag,

18. 'tag_url' => $tag_url

19. ]);

21. $post->tags()->save($tag_ref);

23. } else {

25. $post->tags()->attach($tag_ref->id);

27. }

28. }

29. }

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 :

1. if(isset($inputs['tags']))

2. {

3. $tagRepository->store($post, $inputs['tags']);

4. }

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 :

foreach ($tags as $tag)

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 :

1. $tag_ref = new $this->tag([

2. 'tag' => $tag,

3. 'tag_url' => $tag_url

4. ]);

5. $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 :

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

1. public function destroy(Post $post)

2. {

3. $post->tags()->detach();

4. $post->delete();

5. }

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 :

1. public function getWithUserAndTagsForTagPaginate($tag, $n)

2. {

3. return $this->queryWithUserAndTags()

4. ->whereHas('tags', function($query) use ($tag) {

5. $query->where('tags.tag_url', $tag);

6. })->paginate($n);

7. }

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 :

1. 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')

3. 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

5. select * from `users` where `users`.`id` in ('3', '2', '4')

7. 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')

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

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

4. <meta charset="utf-8">

5. <meta http-equiv="X-UA-Compatible" content="IE=edge">

6. <meta name="viewport" content="width=device-width, initial-scale=1">

8. <!-- CSRF Token -->

9. <meta name="csrf-token" content="{{ csrf_token() }}">

11. <title>Mon joli blog</title>

13. <!-- Styles -->

14. <link href="/css/app.css" rel="stylesheet">

16. <!-- Scripts -->

17. <script>

18. window.Laravel = <?php echo json_encode([

19. 'csrfToken' => csrf_token(),

20. ]); ?>

21. </script>

22. </head>

23. <body>

24. <nav class="navbar navbar-default navbar-static-top">

25. <div class="container">

26. <div class="navbar-header">

28. <!-- Collapsed Hamburger -->

29. <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#app-navbar-collapse">

30. <span class="sr-only">Toggle Navigation</span>

31. <span class="icon-bar"></span>

32. <span class="icon-bar"></span>

33. <span class="icon-bar"></span>

34. </button>

36. <!-- Branding Image -->

37. <a class="navbar-brand" href="{{ URL('/') }}">

38. Mon joli Blog

39. </a>

40. </div>

42. <div class="collapse navbar-collapse" id="app-navbar-collapse">

43. <!-- Left Side Of Navbar -->

44. <ul class="nav navbar-nav">

46. </ul>

48. <!-- Right Side Of Navbar -->

49. <ul class="nav navbar-nav navbar-right">

50. <!-- Authentication Links -->

51. @if (Auth::guest())

52. <li><a href="{{ URL('/login') }}">Se connecter</a></li>

53. <li><a href="{{ URL('/register') }}">S'enregistrer</a></li>

54. @else

55. <li><a href="{{ URL('/post/create') }}">Créer un article</a></li>

56. <li class="dropdown">

57. <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">

58. {{ Auth::user()->name }} <span class="caret"></span>

59. </a>

61. <ul class="dropdown-menu" role="menu">

62. <li>

63. <a href="{{ url('/logout') }}"

64. onclick="event.preventDefault();

65. document.getElementById('logout-form').submit();">

66. Logout

67. </a>

69. <form id="logout-form" action="{{ url('/logout') }}" method="POST" style="display: none;">

70. {{ csrf_field() }}

71. </form>

72. </li>

73. </ul>

74. </li>

75. @endif

76. </ul>

77. </div>

78. </div>

79. </nav>

81. @yield('content')

83. <!-- Scripts -->

84. <script src="/js/app.js"></script>

85. </body>

86. </html>

VIII-F - La liste des articles

Voici la vue pour la liste des articles (resources/views/posts/liste.blade.php) :

1. @extends('layouts.app')

3. @section('content')

4. <div class="container">

5. @if(isset($info))

6. <div class="row alert alert-info">{{ $info }}</div>

7. @endif

8. {!! $posts->links() !!}

9. @foreach($posts as $post)

10. <article class="row bg-primary">

11. <div class="col-md-12">

12. <header>

13. <h1>{{ $post->titre }}

14. <div class="pull-right">

15. @foreach($post->tags as $tag)

16. <a href="{{ URL('post/tag/' . $tag->tag_url) }}" class="btn btn-xs btn-info">{{ $tag->tag }}</a></li>

17. @endforeach

18. </div>

19. </h1>

20. </header>

21. <hr>

22. <section>

23. <p>{{ $post->contenu }}</p>

24. @if(auth()->check() and auth()->user()->admin)

25. <form method="POST" action="{{ route('post.destroy', ['id' => $post->id]) }}">

26. {{ method_field('DELETE') }}

27. {{ csrf_field() }}

28. <input class="btn btn-danger btn-xs" onclick="return confirm('Vraiment supprimer cet article ?')" type="submit" value="Supprimer cet article">

29. </form>

30. @endif

31. <em class="pull-right">

32. {{ $post->user->name }} le {!! $post->created_at->format('d-m-Y') !!}

33. </em>

34. </section>

35. </div>

36. </article>

37. <br>

38. @endforeach

39. {!! $posts->links() !!}

40. </div>

41. @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 :

1. @extends('layouts.app')

3. @section('content')

4. <div class="col-sm-offset-3 col-sm-6">

5. <div class="panel panel-default">

6. <div class="panel-heading">Ajout d'un article</div>

7. <div class="panel-body">

8. <form method="POST" action="{{ URL('/post') }}">

9. {{ csrf_field() }}

10. <div class="form-group{{ $errors->has('titre') ? ' has-error' : '' }}">

11. <input class="form-control" placeholder="Titre" name="titre" type="text" value="{{ old('titre') }}" autofocus>

12. @if ($errors->has('titre'))

13. <span class="help-block">

14. <strong>{{ $errors->first('titre') }}</strong>

15. </span>

16. @endif

17. </div>

18. <div class="form-group{{ $errors->has('contenu') ? ' has-error' : '' }}">

19. <textarea class="form-control" placeholder="Contenu" name="contenu" cols="50" rows="10">{{ old('contenu') }}</textarea>

20. @if ($errors->has('contenu'))

21. <span class="help-block">

22. <strong>{{ $errors->first('contenu') }}</strong>

23. </span>

24. @endif

25. </div>

26. <div class="form-group{{ $errors->has('tags') ? ' has-error' : '' }}">

27. <input class="form-control" placeholder="Entrez les tags séparés par des virgules" name="tags" type="text" value="{{ old('tags') }}">

28. @if ($errors->has('tags'))

29. <span class="help-block">

30. <strong>{{ $errors->first('tags') }}</strong>

31. </span>

32. @endif

33. </div>

34. <button type="submit" class="btn btn-primary pull-right">Envoyer !</button>

35. </form>

37. </div>

38. </div>

39. </div>

40. @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.

IX - Un concepteur de schéma

Dans les chapitres précédents, on a plusieurs fois dû créer des migrations, des populations, des contrôleurs, des modèles… Artisan possède des commandes pour effectuer certaines de ces opérations, mais il nous a fallu créer pas mal de code qui, de toute évidence, pourrait plus ou moins facilement être automatisé.

Il est possible d'améliorer les commandes d'Artisan ou même de s'en créer des nouvelles. Il existe aussi des assistants pour nous aider dans ces tâches un peu pénibles ou répétitives. Je vous propose dans ce chapitre de découvrir le concepteur de schéma Laravel Schema Designer.

IX-A - Mise en route

Laravel Schema Designer est un concepteur de schéma, c'est-à-dire qu'il vous permet de réaliser de façon visuelle des tables avec toutes leurs caractéristiques ainsi que les relations qui les unissent. À l'issue de cette composition graphique, il peut générer le code des migrations, les modèles, et bien d'autres choses…

On va voir comment on l'utilise avec un exemple simple de base bibliographique qui nous servira dans le prochain chapitre.

Mais la première chose que vous devez faire est de créer un compte (gratuit) sur le site.

Ensuite vous donnez un nom à votre base :

Vous pouvez alors commencer à créer !

Le but est d'obtenir ces quatre tables avec ces relations :

On a les relations :

- 1:n entre les éditeurs et les livres ;

- n:n entre les auteurs et les livres.

IX-B - Créer des tables

IX-B-1 - Table des auteurs

Il faut cliquer sur ce lien pour ajouter une table :

Ensuite vous renseignez les champs :

Vous avez alors votre table visuellement à l'écran :

On va ajouter une colonne à cette table, il suffit de cliquer sur le bouton avec le signe + :

Le formulaire est très complet, il suffit de le renseigner :

Après validation on voit notre nouvelle colonne dans la table :

IX-B-2 - Table des éditeurs

Vous allez procéder exactement de la même manière pour la table des éditeurs, on va juste choisir une autre couleur :

Créez aussi une colonne nom avec les mêmes propriétés que celles de la table des auteurs. Finalement vous avez donc la table des éditeurs :

IX-B-3 - Table des livres

Créez la table des livres :

Ajouter les colonnes titre et description (cochez « fillable » pour les deux) :

On va maintenant ajouter la colonne clé étrangère pour les éditeurs :

La table est maintenant complète :

IX-B-4 - Table pivot

Il ne nous manque plus que la table pivot entre les auteurs et les livres (plusieurs auteurs peuvent écrire un livre et un auteur peut écrire plusieurs livres). On va renseigner le formulaire comme on l'a fait pour les autres tables même si on sait qu'on n'aura pas besoin de modèle pour cette table, mais c'est nécessaire pour le fonctionnement du concepteur :

Ajoutez les deux clés étrangères comme on l'a fait ci-dessus (une pour les auteurs et l'autre pour les livres), vous devez donc avoir la table ainsi :

IX-C - Créer les relations

Maintenant que les tables sont prêtes, on va ajouter les relations.

IX-C-1 - Les éditeurs

Commençons par la table des éditeurs. Cliquez sur le bouton le plus à gauche :

Renseignez ainsi le formulaire :

La relation se crée visuellement :

IX-C-2 - Les livres

On va créer la relation réciproque à partir des livres :

On a maintenant les deux relations :

Mais on sait que les livres sont aussi en relation avec les auteurs alors on ajoute cette relation :

IX-C-3 - Les auteurs

Il ne nous manque plus que la relation réciproque entre auteurs et livres :

On a maintenant terminé notre travail de conception. Si vous êtes un peu perdu, j'ai mis le schéma complet en partage ici.

IX-D - La génération

Il ne vous reste plus qu'à cliquer sur ce lien pour tout générer :

Regardez ce que vous recevez :

Ce qui est vraiment intéressant dans tout ça ce sont les migrations et les modèles.

Mettez ces migrations dans une installation fraîche de Laravel en supprimant les deux de base pour les utilisateurs :

Lancez maintenant ces migrations :

Vous créez les tables :

C'est quand même plus sympathique que d'écrire tout le code des migrations.

Récupérez également les modèles qui nous serviront dans le prochain chapitre de ce cours.

IX-E - En résumé

- Le concepteur de schéma « Laravel Schema designer » permet de créer de façon graphique un schéma de base de données.

- On peut créer des tables et des relations.

- La génération finale fournit de nombreux fichiers : migrations, modèles, contrôleurs, vues, routes…

X - Manipuler les données

Dans les précédents chapitres, on a rencontré de nombreux exemples d'utilisation d'Eloquent pour manipuler des données. On a vu que sa capacité d'abstraction permet de réaliser facilement de nombreuses tâches sur les tables.

Avec Eloquent chaque table est représentée par un modèle qui sert à interagir avec elle. On peut ainsi aller chercher des données, en insérer, les modifier, les supprimer…

Il y a aussi dans Laravel un Query Builder qui est une puissante interface pour effectuer des requêtes sur les bases de données. Comme Eloquent et le Query Builder sont intimement liés, on a parfois du mal à les distinguer. En gros Eloquent utilise le Query Builder pour constituer et exécuter les requêtes.

Dans ce chapitre on va faire un peu le point de ce qu'on a vu et on va évoquer d'autres possibilités.

X-A - Les données

Pour effectuer des tests, nous aurons besoin de données. Dans le précédent chapitre, on a construit avec le concepteur de schéma quatre tables reliées par des relations. On a ainsi récupéré des migrations et des modèles. On a aussi utilisé les migrations pour créer les tables dans la base de données. On va avoir besoin de tout ça pour ce chapitre.

Pour rappel voici ce qu'on a dans la base :

Au niveau des relations :

- une relation 1:n entre les éditeurs et les livres ;

- une relation n:n entre les auteurs et les livres.

La structure est en place, mais on va avoir également besoin de données. On va encore utiliser la bibliothèque Faker qui est chargée par défaut dans Laravel.

Modifiez ainsi le code du fichier des fabriques (models factories) :

1. <?php

3. $factory->define(App\Editeur::class, function (Faker\Generator $faker) {

4. return [

5. 'nom' => $faker->name,

6. ];

7. });

9. $factory->define(App\Auteur::class, function (Faker\Generator $faker) {

10. return [

11. 'nom' => $faker->name,

12. ];

13. });

15. $factory->define(App\Livre::class, function (Faker\Generator $faker) {

16. return [

17. 'titre' => $faker->sentence(rand(2, 3)),

18. 'description' => $faker->text,

19. 'editeur_id' => $faker->numberBetween(1, 40),

20. ];

21. });

Ensuite on prévoit ce code dans DatabaseSeeder :

1. <?php

3. use Illuminate\Database\Seeder;

4. use Faker\Factory;

6. class DatabaseSeeder extends Seeder

7. {

8. /**

9. * Run the database seeds.

10. *

11. * @return void

12. */

13. public function run()

14. {

15. factory(App\Editeur::class, 40)->create();

16. factory(App\Auteur::class, 40)->create();

17. factory(App\Livre::class, 80)->create();

19. for ($i = 1; $i < 41; $i++) {

20. $number = rand(2, 8);

21. for ($j = 1; $j <= $number; $j++) {

22. DB::table('auteur_livre')->insert([

23. 'livre_id' => rand(1, 40),

24. 'auteur_id' => $i

25. ]);

26. }

27. }

28. }

29. }

Lancez ensuite la population :

php artisan db:seed

On aura ainsi 40 éditeurs et 40 auteurs avec des noms aléatoires. On aura aussi 80 livres affectés à des éditeurs et aussi des relations entre les livres et les auteurs. Donc de quoi effectuer tranquillement des requêtes.

N'oubliez pas de placer aussi les trois modèles dans l'application.

X-B - Tinker

Artisan est plein de possibilités. Il y a en particulier un outil console, Tinker, qui permet d'effectuer des actions élémentaires. On appelle cela un REPL (Read-Eval-Print Loop). On peut entrer une expression, l'évaluer et on obtient le résultat.

Pour démarrer Tinker c'est tout simple :

Et maintenant il n'y a plus qu'à entrer des expressions, elles seront évaluées dans le contexte de Laravel. Voici un exemple avec notre application :

Tinker est bien pratique pour faire des actions simples de ce genre. Vous pouvez donc l'utiliser s'il vous convient, en particulier pour les exemples de ce chapitre.

X-C - Les sélections simples

L'action la plus fréquente est sans doute le fait d'aller chercher des informations dans la base, on parle de sélection. On a rencontré de nombreux exemples dans les chapitres précédents.

X-C-1 - Liste

Commençons par des choses simples, on veut tous les éditeurs. Avec Eloquent c'est facile :

Les réponses d'Eloquent sont toujours des collections (Illuminate\Database\Eloquent\Collection). Ce sont des objets bien plus puissants et pratiques que de simples tableaux. Pour vous donner une idée des possibilités, regardez toutes les méthodes disponibles. Autrement dit vous avez la possibilité d'effectuer simplement des traitements puissants sur les données retournées par Eloquent.

X-C-2 - Enregistrement particulier

On peut retrouver un éditeur avec son identifiant :

Lorsqu'un seul résultat est retourné, on n'a pas une collection, mais un seul modèle.

On peut aussi le retrouver par son nom :

On peut aussi sélectionner les colonnes qu'on veut :

X-C-3 - Lignes distinctes

On peut aussi utiliser la méthode distinct pour avoir des lignes distinctes :

X-C-4 - Plusieurs conditions

On peut combiner des where :

X-C-5 - Encadrer des valeurs

On peut encadrer des valeurs avec whereBetween :

X-C-6 - Prendre des valeurs dans un tableau

On peut aussi prendre des valeurs dans un tableau avec whereIn :

X-C-7 - Agrégations

On peut aussi compter, calculer…

X-C-8 - Les erreurs

Que se passe-t-il si on ne trouve pas un enregistrement avec find ou first ? Regardez ces exemples :

Donc, si vous voulez générer une erreur Illuminate\Database\Eloquent\ModelNotFoundException utilisez findOrFail ou firstOrFail.

X-D - Les sélections avec plusieurs tables

Pour le moment on a vu des requêtes qui ne concernent qu'une seule table, ce qui n'est pas le plus répandu. Lorsque deux tables sont concernées, on doit classiquement faire une jointure. Mais on a vu qu'Eloquent nous offre des possibilités bien plus pratiques avec les relations.

X-D-1 - Trouver les titres des livres pour un éditeur dont on a l'id

Par exemple pour trouver tous les livres de l'éditeur avec l'identifiant 11 :

Sans Eloquent le Query Builder devrait recourir à une jointure :

Remarquez que le Query Builder retourne lui aussi une collection. C'est une nouveauté bienvenue de la version 5.3.

X-D-2 - Trouver les livres d'un auteur dont on connaît le nom

Maintenant, cherchons les livres d'un auteur dont on connaît le nom. On s'en sort avec la méthode whereHas :

X-E - Attention aux requêtes imbriquées !

Il faut être prudent dans certaines situations. Par exemple, supposez que vous voulez avoir la liste des auteurs avec pour chaque nom d'auteur la liste de ses ouvrages. Vous pourriez écrire ce genre de code :

1. $auteurs = App\Auteur::all();

2. foreach ($auteurs as $auteur) {

3. echo '<h1>' . $auteur->nom . '</h1>';

4. foreach($auteur->livres as $livre) {

5. echo $livre->titre, '<br>';

6. }

7. }

Tout se passe correctement à l'affichage :

Ça fonctionne très bien, mais… si vous regardez vos requêtes, vous allez être effrayé !

Dans mon cas j'en trouve 41 ! Tout simplement parce que pour chaque auteur vous lancez une requête pour trouver ses livres. Dans ce genre de situation il faut absolument utiliser le chargement lié (eager loading), c'est-à-dire demander à Eloquent de charger la table livres avec la méthode with :

1. $auteurs = App\Auteur::with('livres')->get();

2. foreach ($auteurs as $auteur) {

3. echo '<h1>' . $auteur->nom . '</h1>';

4. foreach($auteur->livres as $livre) {

5. echo $livre->titre, '<br>';

6. }

7. }

Le changement peut paraître minime, mais on n'a plus que deux requêtes maintenant :

select * from `auteurs`

select `livres`.*, `auteur_livre`.`auteur_id` as `pivot_auteur_id`, `auteur_livre`.`livre_id` as `pivot_livre_id` from `livres` inner join `auteur_livre` on `livres`.`id` = `auteur_livre`.`livre_id` where `auteur_livre`.`auteur_id` in ('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')

Comment faire la même chose avec le Query Builder ? Ce n'est pas si simple, voici une solution avec utilisation d'une expression brute :

1. $results = DB::table('auteurs')

2. ->select('nom', DB::raw('group_concat(titre) as titres'))

3. ->groupBy('nom')

4. ->join('auteur_livre', 'auteurs.id', '=', 'auteur_livre.auteur_id')

5. ->join('livres', 'livres.id', '=', 'auteur_livre.livre_id')

6. ->get();

7. foreach ($results as $result) {

8. echo '<h1>' . $result->nom . '</h1>';

9. $titres = explode(',', $result->titres);

10. foreach($titres as $titre) {

11. echo $titre, '<br>';

12. }

13. }

Maintenant on n'a plus qu'une seule requête :

select `nom`, group_concat(titre) as titres from `auteurs` inner join `auteur_livre` on `auteurs`.`id` = `auteur_livre`.`auteur_id` inner join `livres` on `livres`.`id` = `auteur_livre`.`livre_id` group by `nom`

Si vous utilisez une expression brute dans une requête (par exemple ci-dessus avec DB::raw) celle-ci n'est pas immunisée contre les injections SQL, vous devez donc prendre vous-même des mesures de sécurité.

Le chargement lié peut même utiliser plusieurs relations successives. Regardez cet exemple :

Ici on charge les livres et les auteurs liés en même temps que les éditeurs en une seule fois. On s'en sort avec trois requêtes :

1. select * from `editeurs` limit 2

3. select * from `livres` where `livres`.`editeur_id` in ('1', '2')

5. select `auteurs`.*, `auteur_livre`.`livre_id` as `pivot_livre_id`, `auteur_livre`.`auteur_id` as `pivot_auteur_id` from `auteurs` inner join `auteur_livre` on `auteurs`.`id` = `auteur_livre`.`auteur_id` where `auteur_livre`.`livre_id` in ('21')

Remarquez l'utilisation de la méthode take pour limiter le nombre de résultats.

Vous n'arriverez pas toujours à réaliser ce que vous désirez avec seulement Eloquent, il vous faudra alors utiliser le Query Builder.

X-F - Insérer des enregistrements

X-F-1 - Méthode « save »

Une façon simple d'ajouter un enregistrement est d'utiliser la méthode save :

On crée une instance du modèle, on renseigne les attributs, on enregistre dans la base avec save. Si tout se passe bien il est retourné true.

Avec cette méthode save, on peut aussi ajouter un enregistrement à travers une relation pour renseigner automatiquement la clé étrangère :

On voit que la colonne editeur_id a été renseignée.

On peut d'ailleurs mettre un tableau de modèles comme argument de la méthode save pour ajouter plusieurs enregistrements d'un coup.

X-F-2 - Méthode « create »

Une autre façon de procéder est d'utiliser la méthode create :

On transmet les attributs sous la forme d'un tableau.

On voit qu'il est retourné une instance du modèle, ce qui est pratique si on veut par exemple connaître l'identifiant de l'enregistrement créé.

Toutefois cette façon de procéder constitue ce qu'on appelle un assignement de masse. Imaginez que le tableau soit constitué de la saisie d'un formulaire et qu'on transmette ainsi le paquet à la méthode. Qu'est-ce qui empêche un utilisateur mal intentionné d'ajouter un attribut ? Pour éviter ça, on doit définir précisément les colonnes qui sont autorisées à être ainsi renseignées avec la méthode $fillable dans le modèle.

Regardez cet exemple :

Apparemment la colonne editeur_id n'est pas renseignée alors qu'on l'a bien prévue dans le tableau. Regardez dans le modèle App\Auteur :

protected $fillable = ['titre', 'description'];

On a prévu seulement titre et description, du coup le reste ne passe pas et on a une erreur à la création.

Il y a des cas où aucune erreur n'est déclenchée et vous ne vous apercevez de rien dans l'immédiat !

Si vous modifiez ainsi la valeur de la propriété :

protected $fillable = ['titre', 'description', 'editeur_id'];

Cette fois ça fonctionne :

Il existe la propriété $guarded qui est exactement l'inverse de $fillable.

À vous de voir quelles colonnes peuvent présenter des risques de sécurité de ce genre !

Avec cette méthode create on peut aussi ajouter un enregistrement à travers une relation pour renseigner automatiquement la clé étrangère :

X-F-3 - Création si un enregistrement n'existe pas

On n'est pas toujours sûr de ce qui se trouve dans la base. Il arrive des fois où on aimerait créer un enregistrement si celui-ci n'existe pas déjà.

On peut évidemment commencer par aller vérifier sa présence et le créer au besoin. Mais Laravel nous offre une possibilité beaucoup plus simple avec les méthodes firstOrNew et firstOrCreate :

À partir de ce scénario, je vous laisse deviner la différence entre les deux méthodes.

X-G - Agir sur la table pivot

X-G-1 - Attacher

Pour ajouter un enregistrement dans la table pivot on dispose de la méthode attach. Par exemple mon auteur 1 est relié à quatre livres :

Je veux le relier à un autre livre, voilà comment faire :

On trouve bien l'enregistrement dans la table pivot :

X-G-2 - Détacher

À l'inverse on peut détacher avec la méthode detach :

X-G-3 - Synchroniser

Parfois on veut mettre à jour globalement les attachements, on le fait avec la méthode sync :

On attache les nouveaux, on détache ce que l'on ne veut plus.

On peut synchroniser sans détacher avec la méthode syncWithoutDetaching.

X-H - Mettre à jour des enregistrements

X-H-1 - Méthode « save »

On peut utiliser la méthode save pour aussi mettre à jour des enregistrements :

X-H-2 - Méthode « update »

On peut aussi utiliser la méthode update :

L'avantage de cette méthode est qu'on peut mettre à jour plusieurs enregistrements :

On voit ici qu'on a modifié cinq enregistrements en changeant l'éditeur de ces livres.

X-I - Supprimer des enregistrements

X-I-1 - Méthode « delete »

On peut utiliser la méthode delete pour supprimer un enregistrement. Étant donné que j'ai prévu de ne pas avoir de suppression en cascade, il devient plus difficile de donner un exemple. Par exemple pour supprimer un auteur il faut commencer par détacher tous ses livres :

Ce chapitre est loin d'épuiser le sujet pour Eloquent et le Query Builder. Vous pourrez trouver tous les compléments utiles sur cette page de la documentation pour le Query Builder et sur cette page et celle-ci pour Eloquent.

X-J - En résumé

- Eloquent permet de faire beaucoup de manipulations sur les tables et est à l'aise avec les relations.

- Le Query Builder est le compagnon parfait pour Eloquent.

- Il faut se méfier des requêtes imbriquées et utiliser le chargement lié (eager loading) pour limiter le nombre de requêtes générées.

- Parfois on doit utiliser des expressions brutes dans les requêtes, mais il faut alors penser à se protéger des injections SQL.

XI - Remerciements

Nous remercions Maurice Chavelli qui nous a autorisés à publier ce tutoriel.

Nous tenons également à remercier Winjerome pour la gabarisation et Claude Leloup pour la correction orthographique.