Cours pour apprendre à utiliser le framework Laravel 5.5

Les données

Maurice Chavelli

Ce Cours fait partie d’une série de tutoriels pour vous apprendre à utiliser le framework Laravel version 5.5. Nous allons 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.

Commentez

- Titre : Cours pour apprendre à utiliser le framework Laravel 5.5

- Auteur : Maurice Chavelli

- Parution : 22 septembre 2018

- Licence : Copyright ® 2018 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. Installation

- I-A-4. Constitution d'une migration

- I-A-5. Lancer une migration

- I-A-6. Annuler ou rafraîchir une migration

- I-A-7. Créer une migration

- I-A-8. Les migrations de l'application d'exemple

- I-B. La population (seeding)

- I-C. Eloquent

- I-D. Routes et contrôleur

- I-D-1. Routes

- I-D-2. Contrôleur

- I-E. Le modèle en détail

- I-E-1. Méthode create et assignement de masse

- I-F. Les vues

- I-G. En résumé

- II. L'authentification

- II-A. La base de données

- II-B. Les middlewares

- II-B-1. Middleware auth

- II-B-2. Middleware guest

- II-C. Les routes de l'authentification

- II-D. Les vues de l'authentification

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

- II-E-1. La vue register

- II-E-2. La validation

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

- II-E-4. La connexion d'un utilisateur

- II-E-5. La vue login

- II-E-6. La validation

- II-E-7. La redirection

- II-E-8. La déconnexion d'un utilisateur

- II-F. L'authentification dans l'application d'exemple

- II-F-1. Les vues personnalisées

- II-F-2. La confirmation de l'email

- II-F-3. La connexion avec l'email ou le nom d'utilisateur

- II-G. En résumé

- III. L'oubli du mot de passe

- III-A. La base de données

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

- III-C. Deuxième étape : le renouvellement

- III-D. Le renouvellement du mot de passe avec l'application d'exemple

- III-E. En résumé

- IV. Jouer avec Eloquent

- IV-A. Tinker

- IV-B. Créer un enregistrement

- IV-B-1. Méthode create

- IV-B-2. Méthode save

- IV-C. Modifier un enregistrement

- IV-C-1. Méthode update

- IV-C-2. Méthode save

- IV-D. Supprimer un enregistrement

- IV-E. Trouver des enregistrements

- IV-E-1. Tous les enregistrements

- IV-E-2. Sélectionner des enregistrements

- IV-F. Trouver ou créer

- IV-G. L'application d'exemple

- IV-G-1. Les articles sur la page d'accueil

- IV-G-2. La modification des utilisateurs

- IV-H. En résumé

- V. La relation 1:n

- V-A. Les migrations

- V-A-1. La table users

- V-A-2. La table posts

- V-A-3. La migration des clés étrangères

- V-B. La relation

- V-C. Les modèles et la relation

- V-C-1. Le modèle User

- V-C-2. Le modèle Post

- V-C-3. La relation 1:n

- V-D. Jouer avec la relation

- V-D-1. L'eager loading

- V-D-2. Tester la présence de la relation

- V-D-3. Compter les enregistrements en relation

- V-E. L'application d'exemple

- V-E-1. Affichage d'un article

- V-E-2. La liste des articles dans l'administration

- V-F. La population (seeding)

- V-G. En résumé

- VI. La relation n:n

- VI-A. La relation n:n

- VI-B. Les catégories et les articles

- VI-B-1. Présentation

- VI-B-2. Les données

- VI-B-3. Les migrations

- VI-B-4. Les modèles

- VI-B-5. La relation n:n

- VI-C. Les articles et les tags

- VI-C-1. Présentation

- VI-C-2. Les données

- VI-C-3. Les modèles

- VI-C-4. La relation n:n

- VI-D. Jouer avec les relations

- VI-D-1. Attacher et détacher

- VI-D-2. Synchroniser

- VI-D-3. Échanger (toggle)

- VI-E. L'application d'exemple

- VI-E-1. Enregistrement d'un article

- VI-E-1-a. Présentation

- VI-E-1-b. Route et contrôleur

- VI-E-1-c. La validation

- VI-E-1-d. Le repository

- VI-E-1-e. La vue

- VI-E-2. La population (seeding)

- VI-F. En résumé

- VII. Le polymorphisme

- VII-A. Un peu de théorie

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

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

- VII-A-3. La relation une table vers plusieurs tables

- VII-A-3-a. Type de relation 1:n

- VII-A-3-b. Type de relation n:n

- VII-B. L'application d'exemple

- VII-B-1. Présentation

- VII-B-2. Les modèles

- VII-B-3. La création des ingoings

- VII-B-4. Les panneaux de l'administration

- VII-B-5. Suppression des ingoings

- VII-C. En résumé

- VIII. Gérer un arbre

- VIII-A. Aspect fonctionnel

- VIII-B. Installer un package

- VIII-C. Les données

- VIII-C-1. Les migrations

- VIII-C-2. Les modèles

- VIII-C-3. Les relations

- VIII-D. Création d'un commentaire

- VIII-D-1. Les routes

- VIII-D-2. Le contrôleur

- VIII-E. Modification d'un commentaire

- VIII-F. Suppression d'un commentaire

- VIII-G. Affichage des commentaires

- VIII-H. Afficher plus de commentaires

- VIII-I. La population (seeding)

- VIII-J. En résumé

- IX. La pagination

- IX-A. Paginer (paginate) le résultat d'une requête

- IX-B. Ajouter (appends) des paramètres à la pagination

- IX-C. La pagination en Ajax

- IX-D. La personnalisation de la vue de la pagination

- IX-E. En résumé

- X. Les ressources

- X-A. Préparation

- X-B. Les contrôleurs de ressource

- X-B-1. Le contrôleur

- X-B-2. Les routes

- X-B-3. Un peu de code

- X-B-3-a. Liste des utilisateurs

- X-B-3-b. Afficher un utilisateur

- X-B-3-c. Créer un utilisateur

- X-B-3-d. Modifier un utilisateur

- X-B-3-e. Supprimer un utilisateur

- X-C. L'application d'exemple

- X-D. Les API REST

- X-D-1. Le contrôleur

- X-D-2. Les routes

- X-D-3. Le fonctionnement

- X-E. Les ressources d'API

- X-F. En résumé

- XI. Les assistants

- XI-A. Laravel Schema Designer

- XI-B. InfyOm

- XI-C. MySQL Workbench Export

- XI-D. Il n'y a pas qu'Eloquent

- XI-D-1. Laravel Doctrine

- XI-D-2. Laravel MongoDB

- XI-E. En résumé

I - Migrations et modèles

Dans cette deuxième partie du cours je vais me référer à l'application d'exemple présente sur Github plutôt que d'élaborer des mini-applications pas trop réalistes. On va ainsi progresser dans l'apprentissage de Laravel en décortiquant cette application très complète. Il vous faut donc l'installer en suivant la procédure indiquée…

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 et 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. Il faudra évidemment modifier ces valeurs selon votre contexte de développement et définir surtout le nom de la base, le nom de l'utilisateur et le mot de passe. Pour une installation de MySQL en local en général l'utilisateur est root et on n'a pas de mot de passe.

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 :

On dispose de six commandes :

- fresh : supprime toutes les tables et relance la migration (commande apparue avec la version 5.5) ;

- install : crée et informe la table de référence des migrations ;

- refresh : réinitialise et relance les migrations ;

- rollback : annule la dernière migration ;

- status : donne des informations sur les migrations.

I-A-3 - Installation

Si vous regardez dans le dossier database/migrations il y a déjà 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é.

Puisque ces migrations sont présentes autant les utiliser pour nous entraîner.

Commencez par créer une base MySQL et informez .env, par exemple :

1. DB_CONNECTION=mysql

2. DB_HOST=127.0.0.1

3. DB_PORT=3306

4. DB_DATABASE=laravel5

5. DB_USERNAME=root

6. DB_PASSWORD=

Lancez alors la commande install :

On se retrouve alors avec une table migrations dans la base avec cette structure :

Pour le moment cette table est vide, elle va se remplir au fil des migrations pour les garder en mémoire.

Vous n'aurez jamais à intervenir directement sur cette table qui est là juste pour la gestion effectuée par Laravel.

I-A-4 - Constitution d'une migration

Si vous ouvrez le fichier database/migrations/2014_10_12_000000_create_users_table.php vous trouvez ce 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::dropIfExists('users');

34. }

35. }

On dispose dans cette classe de deux fonctions :

- up : ici on a le code de création de la table et de ses colonnes ;

- down : ici on a le code de suppression de la table.

I-A-5 - Lancer une migration

Pour lancer toutes les migrations on utilise la commande migrate :

On voit que les deux migrations présentes ont été exécutées et on trouve les deux tables dans la base :

Pour comprendre le lien entre migration et création de la table associée voici une illustration pour la table users :

La méthode timestamps permet la création des deux colonnes created_at et updated_at.

I-A-6 - Annuler ou rafraîchir une migration

Pour annuler une migration on utilise la commande rollback :

Les méthodes down des migrations sont exécutées et les tables sont supprimées.

Pour annuler et relancer en une seule opération on utilise la commande refresh :

Pour éviter d'avoir à coder ces méthodes down, la version 5.5 a prévu la commande fresh qui supprime automatiquement les tables concernées.

I-A-7 - Créer une migration

Il existe une commande d'Artisan pour créer un squelette de migration :

La migration est créée dans le dossier :

Avec ce code de base :

1. <?php

3. use Illuminate\Support\Facades\Schema;

4. use Illuminate\Database\Schema\Blueprint;

5. use Illuminate\Database\Migrations\Migration;

7. class Test 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. }

Il faut ensuite compléter ce code selon vos besoins !

Le nom du fichier de migration commence par sa date de création, ce qui conditionne son positionnement dans la liste. L'élément important à prendre en compte est que l'ordre des migrations prend une grande importance lorsqu'on a des clés étrangères !

I-A-8 - Les migrations de l'application d'exemple

L'application d'exemple comporte de nombreuses migrations :

Pour le présent chapitre nous allons nous intéresser à la migration de la table contacts :

1. <?php

3. use Illuminate\Database\Migrations\Migration;

4. use Illuminate\Database\Schema\Blueprint;

6. class CreateContactsTable extends Migration {

8. public function up()

9. {

10. Schema::create('contacts', function(Blueprint $table) {

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

12. $table->timestamps();

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

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

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

16. });

17. }

19. public function down()

20. {

21. Schema::drop('contacts');

22. }

23. }

Pour cette table on a :

- une colonne clé incrémentée id (increments) ;

- les colonnes created_at et updated_at créées par la méthode timestamps ;

- une colonne name de type varchar (string) ;

- une colonne email de type varchar (string) ;

- une colonne text de type text (text).

Lorsque vous lancez les migrations la table contacts est créée :

Cette table est destinée dans l'application à mémoriser les messages laissés par les visiteurs lorsqu'ils accèdent au formulaire avec cet item du menu :

I-B - La population (seeding)

En plus de proposer des migrations Laravel permet aussi la population (seeding), c'est-à-dire un moyen simple de remplir les tables d'enregistrements. Les classes de la population se trouvent dans le dossier databases/seeds:

On est libre d'organiser les classes comme on veut dans ce dossier. Dans l'application d'exemple il n'y a qu'une classe.

Lorsqu'on installe Laravel on dispose de la classe DatabaseSeeder avec 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. }

On a un appel commenté à une classe UserTableSeeder qui pourrait être une classe pour remplir la table users.

Dans la fonction run on met tout le code qu'on veut pour remplir les tables. Dans l'application d'exemple on a :

1. User::create(

2. [

3. 'name' => 'GreatAdmin',

4. 'email' => 'admin@la.fr',

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

6. 'role' => 'admin',

7. 'valid' => true,

8. 'confirmed' => true,

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

10. ]

11. );

Ici on crée l'enregistrement d'un utilisateur dans la table users avec l'ORM Eloquent dont je vais parler ci-dessous.

Je reviendrai dans ce cours sur la population quand on aura avancé notre connaissance des outils pour les bases de données. Je vais juste ajouter qu'on lance la population avec cette commande :

php artisan db:seed

I-C - Eloquent

Laravel propose un ORM (acronyme de object-relational mapping ou en bon français une correspondance 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. On peut créer un modèle avec Artisan :

php artisan make:model Test

On trouve le fichier ici :

Avec cette trame de base :

1. <?php

3. namespace App;

5. use Illuminate\Database\Eloquent\Model;

7. class Test extends Model

8. {

9. //

10. }

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

php artisan make:model Test -m

Dans l'application d'exemple les modèles sont rassemblés dans un dossier spécifique dans un souci d'organisation :

Ce modèle Contact correspond par défaut à la table contacts. Eloquent a quelques conventions de ce genre qui simplifient le codage. Si le nom de la table ne répond pas à la convention il faut prévoir une propriété $table pour préciser ce nom.

I-D - Routes et contrôleur

I-D-1 - Routes

Pour les contacts dans l'application il faut deux routes :

- une route pour envoyer le formulaire à l'utilisateur ;

- une route pour la validation du formulaire.

Si vous regardez dans le fichier routes/web.php vous trouvez cette ligne de code :

Route::resource('contacts', 'Front\ContactController', ['only' => ['create', 'store']]);

La méthode resource crée par défaut ces sept routes CRUD :

Verbe

URI

Action

Nom de la route

GET

/contacts

index

contacts.index

GET

/contacts/create

create

contacts.create

POST

/contacts

store

contacts.store

GET

/contacts/{contact}

show

contacts.show

GET

/contacts/{contact}/edit

edit

contacts.edit

PUT/PATCH

/contacts/{contact}

update

contacts.update

DELETE

/contacts/{contact}

destroy

contacts.destroy

Il existe une commande d'Artisan pour créer un contrôleur qui digère directement toutes ces routes :

php artisan make:controller ContactController --resource

Je reviendrai sur ce point dans un chapitre ultérieur. Pour le moment notre contrôleur de contact se contentera de deux méthodes (create et store), c'est pour cette raison qu'on utilise le only dans la syntaxe de la route pour générer seulement les deux routes correspondantes. On aurait aussi pu détailler les deux routes :

Route::name('contacts.create')->get('contacts', 'Front\ContactController@create');
Route::name('contacts.store')->post('contacts', 'Front\ContactController@store');

I-D-2 - Contrôleur

Le contrôleur est rangé ici :

Avec ce code :

1. <?php

3. namespace App\Http\Controllers\Front;

5. use App\ {

6. Http\Controllers\Controller,

7. Http\Requests\ContactRequest,

8. Models\Contact

9. };

11. class ContactController extends Controller

12. {

13. /**

14. * Create a new ContactController instance.

15. *

16. */

17. public function __construct()

18. {

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

20. }

22. /**

23. * Show the form for creating a new contact.

24. *

25. * @return \Illuminate\Http\Response

26. */

27. public function create()

28. {

29. return view ('front.contact');

30. }

32. /**

33. * Store a newly created contact in storage.

34. *

35. * @param  ContactRequest $request

36. * @return \Illuminate\Http\Response

37. */

38. public function store(ContactRequest $request)

39. {

40. Contact::create ($request->all ());

42. return back ()->with ('ok', __('Your message has been recorded, we will respond as soon as possible.'));

43. }

44. }

La méthode create ne présente aucune nouveauté : elle se contente de renvoyer une vue avec le formulaire.

La méthode store est chargée de mémoriser les informations dans la table contacts. On voit qu'on utilise la méthode create du modèle pour le réaliser. Il suffit de donner comme paramètres à cette méthode les entrées du formulaire.

Remarquez qu'on utilise une requête de formulaire (ContactRequest) pour la validation avec des règles classiques :

1. public function rules()

2. {

3. return [

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

5. 'email' => 'bail|required|email',

6. 'message' => 'bail|required|max:1000'

7. ];

8. }

I-E - Le modèle en détail

Modifiez ainsi le code dans le contrôleur ContactController :

dd(Contact::create ($request->all ()));

L'helper dd est bien pratique : il regroupe un var_dump et un die. Ici si on soumet le formulaire on va observer de plus près le modèle créé parce que la méthode create renvoie ce modèle  :

1. Contact {#302 ▼

2. #dispatchesEvents: array:1 [▶️]

3. #fillable: array:3 [▼

4. 0 => "name"

5. 1 => "email"

6. 2 => "message"

7. ]

8. #connection: "mysql"

9. #table: null

10. #primaryKey: "id"

11. #keyType: "int"

12. +incrementing: true

13. #with: []

14. #withCount: []

15. #perPage: 15

16. +exists: true

17. +wasRecentlyCreated: true

18. #attributes: array:6 [▼

19. "name" => "Durand"

20. "email" => "durand@chezlui.fr"

21. "message" => "Mon message"

22. "updated_at" => "2017-09-11 15:28:59"

23. "created_at" => "2017-09-11 15:28:59"

24. "id" => 7

25. ]

26. #original: array:6 [▶️]

27. #changes: []

28. #casts: []

29. #dates: []

30. #dateFormat: null

31. #appends: []

32. #observables: []

33. #relations: []

34. #touches: []

35. +timestamps: true

36. #hidden: []

37. #visible: []

38. #guarded: array:1 [▶️]

39. }

On a pas mal de propriétés mais surtout des attributs (attributes) et on se rend compte que chacun de ces attributs correspond à une colonne de la table contact :

I-E-1 - Méthode create et assignement de masse

Par sécurité ce type d'assignement de masse (on transmet directement un tableau de valeurs issues du client avec la méthode create) est limité pour la sécurité 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/Models/Contact.php vous trouvez cette propriété :

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

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

Mais quel est le risque ?

Imaginez qu'il y ait une autre colonne avec des données sensibles et non prévue dans le formulaire mais qu'un petit malin l'ajoute à la requête, cette colonne serait mise à jour en même temps que les autres !

I-F - Les vues

Je ne vais pas détailler les vues dans ce chapitre consacré aux données. Je signale juste que les vues sont rangées dans des dossiers :

Le template pour les vues du front est layout.blade.php. Le template utilisé est issu de cette source :

Je l'ai évidemment adapté pour Laravel mais je consacrerai un chapitre sur le sujet parce que de nombreuses possibilités de Blade ont été utilisées.

Le formulaire de contact a cet aspect :

I-G - 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.

- L'assignement de masse est limité par la propriété $fillable.

II - 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.

II-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 on a vu dans le chapitre précédent qu'il existe déjà deux migrations :

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.

- table users : par défaut Laravel considère que cette table existe et il s'en sert comme référence pour l'authentification ;

- table password_resets : cette table va nous servir pour la réinitialisation des mots de passe.

II-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.

II-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 la 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::middleware('auth')->group(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. });

Le groupement de routes avec la méthode group permet de mutualiser des actions et de simplifier le code.

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. }

II-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, comme d'ailleurs tous les middlewares, peut s'utiliser sur une route, un groupe de route ou dans le constructeur d'un contrôleur, avec la même syntaxe.

II-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 le contrôleur HomeController a aussi été créé:

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.

II-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.

II-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. @auth

4. <a href="{{ url('/home') }}">Home</a>

5. @else

6. <a href="{{ route('login') }}">Login</a>

7. <a href="{{ route('register') }}">Register</a>

8. @endauth

9. </div>

10. @endif

On en profite pour voir les opérateurs conditionnels de Blade.

Avec un @if on vérifie si dans les routes (Route) on a (has) une route login et si c'est le cas on vérifie si l'utilisateur est authentifié avec @auth ;si c'est le cas on affiche un lien sur la page home et si ce n'est pas le cas (@else) on affiche les deux liens de l'authentification.

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 nom 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.

II-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. @guest

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

3. <li><a href="{{ route('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. @endguest

On utilise @guest si on a affaire à un utilisateur non authentifié. En effet dans ce cas on génère les liens pour le login et l'enregistrement.

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. On pourrait utiliser l'helper auth :

auth()->user()->name

II-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|string|max:255',

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

6. 'password' => 'required|string|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 :

II-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. }

On utilise la méthode create d'Eloquent comme on l'a déjà vu au chapitre précédent. Vous trouvez dans le modèle App\User.php la propriété $fillable renseignée :

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

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 :

II-E-4 - 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 nom 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. }

II-E-5 - 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.

II-E-6 - 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|string',

5. 'password' => 'required|string',

6. ]);

7. }

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.

En plus de la validation il est effectué une vérification de la présence du couple email/password (credentials) dans la base de données :

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.

II-E-7 - 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. if (method_exists($this, 'redirectTo')) {

4. return $this->redirectTo();

5. }

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

8. }

Par défaut on redirige sur home ou sur la valeur de la propriété ou la méthode redirectTo si l'une d'elles existe. Donc pour changer la redirection par défaut il suffit de créer cette propriété ou cette méthode 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.

II-E-8 - 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. Du coup la vue utilise un formulaire et une soumission en JavaScript :

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

2. onclick="event.preventDefault();

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

4. Logout

5. </a>

7. ...

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

10. {{ csrf_field() }}

11. </form>

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()->invalidate();

7. return redirect('/');

8. }

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

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

II-F - L'authentification dans l'application d'exemple

L'application d'exemple utilise les routes générées par Laravel. Par contre il y a plusieurs adaptations :

- les vues sont personnalisées ;

- un package ajoute la confirmation de l'email ;

- on peut se connecter avec l'email ou le nom d'utilisateur.

II-F-1 - Les vues personnalisées

Il est évident qu'on doit adapter ses vues au design de son site. Les vues ont cet aspect :

II-F-2 - La confirmation de l'email

La confirmation de l'email se fait avec le package laravel-email-confirmation.

II-F-3 - La connexion avec l'email ou le nom d'utilisateur

Voici le code du contrôleur LoginController de l'application :

1. <?php

3. namespace App\Http\Controllers\Auth;

5. use App\Http\Controllers\Controller;

6. use Bestmomo\LaravelEmailConfirmation\Traits\AuthenticatesUsers;

7. use Illuminate\Http\Request;

9. class LoginController extends Controller

10. {

11. /*

12. |--------------------------------------------------------------------------

13. | Login Controller

14. |--------------------------------------------------------------------------

15. |

16. | This controller handles authenticating users for the application and

17. | redirecting them to your home screen. The controller uses a trait

18. | to conveniently provide its functionality to your applications.

19. |

20. */

22. use AuthenticatesUsers;

24. /**

25. * Where to redirect users after login.

26. *

27. * @var string

28. */

29. protected $redirectTo = '/';

31. /**

32. * Create a new controller instance.

33. *

34. */

35. public function __construct()

36. {

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

38. }

40. /**

41. * Get the login username to be used by the controller.

42. *

43. * @return string

44. */

45. public function username()

46. {

47. return 'log';

48. }

50. /**

51. * Get the needed authorization credentials from the request.

52. *

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

54. * @return array

55. */

56. protected function credentials(Request $request)

57. {

58. $logValue = $request->input($this->username());

60. $logKey = filter_var($logValue, FILTER_VALIDATE_EMAIL) ? 'email' : 'name';

62. return [

63. $logKey => $logValue,

64. 'password' => $request->input('password'),

65. ];

66. }

67. }

On a la surcharge des deux méthodes username et credentials du trait AuthenticatesUsers. Le champ de saisie s'appelle log dans la vue. On utilise la fonction filter_var de PHP pour définir si la valeur saisie est un email ou pas.

II-G - 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.

- On peut ajouter la confirmation de l'email avec un package dédié.

- On peut permettre la connexion indifféremment avec l'email ou le nom d'utilisateur.

III - 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.

III-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. ],

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

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 nom 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 ce trait 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. 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 :

return $response == Password::RESET_LINK_SENT
? $this->sendResetLinkResponse($response)
: $this->sendResetLinkFailedResponse($request, $response);

Voici ce que ça donne :

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

Il sera possible de vérifier la provenance de la demande.

III-C - 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 nom 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="34c66caad4a6b801dcc6347c530063e7835dd67f280f46ecba8f7a0f76c07c8d">

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 :

$this->validate($request, $this->rules(), $this->validationErrorMessages());

On trouve les règles (rules) dans cette méthode :

1. protected function rules()

2. {

3. return [

4. 'token' => 'required',

5. 'email' => 'required|email',

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

7. ];

8. }

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. );

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

III-D - Le renouvellement du mot de passe avec l'application d'exemple

Le renouvellement du mot de passe dans l'application d'exemple suit exactement le processus vu dans ce chapitre en utilisant le même code. La seule différence tient dans le fait que les vues ont été adaptées au design du site ainsi que les messages :

III-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.

IV - Jouer avec Eloquent

On a commencé à voir Eloquent et les modèles dans le premier chapitre de cette partie du cours, mais on s'est limité pour le moment à créer un enregistrement. Dans ce chapitre on va aller plus loin en explorant les possibilités d'Eloquent, couplé à un puissant générateur de requêtes, pour manipuler les données.

Pour effectuer les manipulations de ce chapitre vous aurez besoin d'une installation fraîche de Laravel complétée avec les éléments de l'authentification comme on l'a vu dans le chapitre correspondant. Les migrations devront également être effectuées pour qu'on puisse utiliser la base de données. Vous aurez également besoin de l'application d'exemple fonctionnelle.

IV-A - Tinker

Artisan est plein de possibilités, l'une d'elles est un outil très pratique, un REPL (Read-Eval-Print Loop). C'est un outil qui permet d'entrer des expressions, de les faire évaluer et d'avoir le résultat à l'affichage et son nom est Tinker. Ce REPL est boosté par le package psysh. Il y a un bon article de présentation en anglais ici.

Pour lancer cet outil il y a une commande d'artisan :

php artisan tinker

On va se servir de cet outil pour interagir avec la base de données dans ce chapitre.

IV-B - Créer un enregistrement

IV-B-1 - Méthode create

On a déjà vu comment créer un enregistrement avec Eloquent. Comme a priori votre base de données est vide avec une installation fraîche de Laravel on va commencer par ajouter un utilisateur dans la table users à l'aide du modèle User. Pour mémoire ce modèle est le seul présent au départ :

Dans Tinker entrez cette expression :

User::create(['name' => 'Durand', 'email' => 'durand@chezlui.fr', 'password' => 'pass'])

Remarquez qu'on n'a pas besoin d'entrer l'espace de nom complet App\User, Tinker crée automatiquement un alias.

La méthode create renvoie le modèle avec tous les attributs créés comme les dates et l'id. Tinker nous les affiche gentiment. On retrouve évidemment notre enregistrement dans la table users :

Pour mémoire cette méthode create est un assignement de masse et implique les précautions que nous avons déjà vues.

IV-B-2 - Méthode save

Une autre façon de créer un enregistrement avec Eloquent est de commencer par créer un modèle vide, renseigner les attributs, puis utiliser la méthode save pour enregistrer dans la table.

Entrez cette suite d'expressions :

1. $user = new User

2. $user->name = 'Dupont'

3. $user->email = 'dupont@chezlui.fr'

4. suser->password = 'pass'

5. $user->save()

On commence par créer le modèle, puis on renseigne ses attributs, enfin on enregistre dans la base avec save.

IV-C - Modifier un enregistrement

Voyons maintenant comment modifier un enregistrement existant. Il y a aussi deux façons de procéder.

IV-C-1 - Méthode update

Entrez cette expression dans Tinker :

User::find(1)->update(['email' => 'durand@cheznous.fr'])

On commence par récupérer le premier enregistrement de la table avec find(1), ce qui a pour effet de créer un modèle avec les attributs renseignés pour Durand. Ensuite on utilise la méthode update sur ce modèle en précisant le nom et la valeur de chaque attribut qu'on veut modifier dans un tableau. Ici on a changé l'adresse email pour Durand.

Avec cette méthode update on a encore le problème de l'assignement de masse.

IV-C-2 - Méthode save

On peut aussi utiliser la méthode save pour faire une modification. Entrez ces expressions dans Tinker :

$user = User::find(2)
$user->email = 'dupont@cheznous.fr'
$user->save()

Avec find(2) on crée un modèle avec les renseignements de Dupont (le deuxième enregistrement de la table), ensuite on change l'attribut email, enfin on enregistre dans la base.

On peut constater les modifications dans la table :

Remarquez que les colonnes created_at et updated_at n'ont maintenant plus les mêmes valeurs puisqu'on a fait des modifications.

IV-D - Supprimer un enregistrement

Après la création et la modification vient tout naturellement la suppression avec la méthode delete. Si vous entrez cette expression dans Tinker :

User::find(2)->delete()

Vous supprimez l'utilisateur qui a l'index 2.

La suppression est définitive. Il existe aussi une possibilité de suppression provisoire (soft delete) mais il faut modifier le modèle en ajoutant un trait et une propriété. Cette partie de la documentation traite de ce sujet que je ne pense pas développer dans ce cours.

IV-E - Trouver des enregistrements

Rafraîchissez la table (pas dans Tinker cette fois) :

php artisan migrate:refresh

Et recréez avec Tinker les deux utilisateurs :

User::create(['name' => 'Durand', 'email' => 'durand@chezlui.fr', 'password' => 'pass'])
User::create(['name' => 'Dupont', 'email' => 'dupont@chezlui.fr', 'password' => 'pass'])

IV-E-1 - Tous les enregistrements

On a vu déjà la méthode find pour trouver un enregistrement à partir de son identifiant (on peut aussi passer plusieurs identifiants dans un tableau). On peut aussi tous les récupérer avec la méthode all :

Si vous êtes observateur vous avez remarqué que cette fois on n'obtient pas un modèle, comme c'était le cas avec find mais une collection (Illuminate\Database\Eloquent\Collection). Cette classe comporte un nombre considérable de méthodes, on peut donc effectuer de nombreux traitements à ce niveau et même les chaîner.

Par exemple ici j'extrais un modèle au hasard de la collection :

J'aurai l'occasion dans ce cours de montrer l'utilisation de plusieurs des méthodes disponibles.

IV-E-2 - Sélectionner des enregistrements

En général on ne veut pas tous les enregistrements mais juste certains qui correspondent à certains critères. Eloquent est couplé à un puissant générateur de requêtes SQL (Query Builder). On peut par exemple utiliser la méthode where. Entrez cette expression dans Tinker :

User::where('name', 'Dupont')->get()

On récupère une collection qui ne contient que le modèle qui correspond à Dupont.

Ajoutez cet utilisateur :

User::create(['name' => 'Martin', 'email' => 'martin@chezlui.fr', 'password' => 'pass'])

On a maintenant trois utilisateurs :

Maintenant entrez cette expression :

User::where('name', '<', 'e')->get()

Cette fois on a sélectionné à partir des noms dont la première lettre doit être avant « e » dans l'alphabet et on obtient une collection avec deux modèles.

On dispose de la méthode first si on veut récupérer juste le premier :

On peut aussi limiter les colonnes retournées avec la méthode select :

Ici on ne retourne que les noms.

Souvent on a envie de connaître la requête générée, pour la voir il suffit d'utiliser la méthode toSql :

On dispose avec le générateur de requêtes de riches possibilités que nous verrons progressivement, par exemple on peut facilement trier les enregistrements avec la méthode orderBy :

Souvent il existe une syntaxe plus simple, par exemple on peut obtenir le même résultat avec latest :

User::select('name')->latest('name')->get()

IV-F - Trouver ou créer

Que se passe-t-il si l'enregistrement qu'on veut ne se trouve pas dans la table ? Faisons un essai :

On a null en retour et on peut gérer cette valeur. Mais parfois on se trouve dans une situation où on veut récupérer un enregistrement ou le créer s'il n'existe pas. On peut le faire en deux étapes mais on peut aussi le faire en une seule action avec la méthode firstOrCreate :

Ici comme l'enregistrement existe il est retourné normalement mais dans le cas suivant il n'existe pas et donc il est créé :

On se retrouve donc avec quatre enregistrements dans la table :

Il existe aussi la méthode firstOrNew, si l'enregistrement existe il est retourné comme avec firstOrCreate, par contre s'il n'existe pas une instance du modèle est créée mais aucune action dans la base n'est réalisée.

De la même manière il existe la méthode updateOrCreate basée sur le même principe.

IV-G - L'application d'exemple

Dans l'application d'exemple il y a sept modèles et un trait :

La plupart des opérations sur la base sont déléguées à des repositories :

Ces modèles et repositories sont intensivement utilisés et nous allons voir quelques illustrations concrètes des éléments théoriques vus ci-dessus.

IV-G-1 - Les articles sur la page d'accueil

La page d'accueil présente un résumé des articles avec une image et un texte d'introduction :

Il faut aller extraire ces informations de la base. D'autre part on a aussi une pagination pour limiter le nombre d'articles visibles sur une page mais je développerai ce point particulier dans un chapitre spécifique parce que c'est un sujet assez long.

Dans le fichier PostRepository vous trouvez ce code :

1. public function __construct(Post $post, Tag $tag, Comment $comment)

2. {

3. $this->model = $post;

4. ...

5. }

8. protected function queryActiveOrderByDate()

9. {

10. return $this->model

11. ->select('id', 'title', 'slug', 'excerpt', 'image')

12. ->whereActive(true)

13. ->latest();

14. }

17. public function getActiveOrderByDate($nbrPages)

18. {

19. return $this->queryActiveOrderByDate()->paginate($nbrPages);

20. }

Dans la fonction queryActiveOrderByDate vous avez des choses qu'on a vues ci-dessus :

- select : pour ne sélectionner que les colonnes qui nous intéressent, en effet on ne veut pas toutes les informations des articles pour cette page d'accueil ;

- whereActive : on ne sélectionne que les articles « actifs », c'est la colonne active qui nous l'indique ;

- latest : on trie les articles du plus récent au plus ancien (équivalent à orderBy('created_at', desc)).

Ensuite dans la fonction getActiveOrderByDate on se contente d'appliquer la pagination.

Au niveau du contrôleur PostController le code est classique :

1. public function index()

2. {

3. $posts = $this->postRepository->getActiveOrderByDate($this->nbrPages);

5. return view('front.index', compact('posts'));

6. }

On retourne la vue front.index (qui donc se trouve en resources/views/front/index.blade.php) en lui transmettant le résultat de la requête, c'est-à-dire un tableau qui contient une collection de tous les enregistrements des articles.

Au niveau de la vue l'instruction @foreach de Blade permet d'itérer dans les collections pour toutes les récupérer :

@foreach($posts as $post)
@include('front.brick-standard', compact('$post'))
@endforeach

Avec Blade on peut inclure une vue dans une autre vue avec l'instruction @include. C'est ce qui est réalisé ici avec l'inclusion de la vue front.brick-standard (qui donc se trouve en resources/views/front/brick-standard.blade.php) :

1. <article class="brick entry format-standard animate-this">

3. <div class="entry-thumb">

4. <a href="{{ url('posts/' . $post->slug) }}" class="thumb-link"><img src="{{ $post->image }}"></a>

5. </div>

7. <div class="entry-text">

8. <div class="entry-header">

9. <h1 class="entry-title"><a href="{{ url('posts/' . $post->slug) }}">{{ $post->title }}</a></h1>

10. </div>

11. <div class="entry-excerpt">

12. {{ $post->excerpt }}

13. </div>

14. </div>

16. </article>

Pour chaque article ce code est généré et on voit que les propriétés sont récupérées de façon classique, par exemple pour le titre avec $post->title.

L'application utilise le package barryvdh/laravel-debugbar qui permet en particulier de surveiller les requêtes générées :

On aura de nombreuses occasions d'utiliser cette barre et n'hésitez pas à en explorer les possibilités. Elle ne fonctionne que lorsqu'on est en mode DEBUG.

IV-G-2 - La modification des utilisateurs

Lorsqu’on se connecte en tant qu'administrateur, et qu'on essaie d’éditer un utilisateur via l'administration des utilisateurs, on a ce formulaire :

Si vous allez dans le fichier UserRepository vous trouvez ce code :

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

2. {

3. $inputs = $request->all ();

5. if (isset($inputs['confirmed'])) {

6. $inputs['confirmed'] = true;

7. }

9. if (isset($inputs['valid'])) {

10. $inputs['valid'] = true;

11. }

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

15. ...

16. }

Voici les actions réalisées :

- on récupère toutes les entrées du formulaire $request->all() dans le tableau $inputs, ;

- si la case à cocher « Confirmé » est cochée on ajoute la colonne confirmed avec la valeur true (on sait que si une case n'est pas cochée dans un formulaire aucune information n'est envoyée) ;

- si la case à cocher « Valide » est cochée on ajoute la colonne valid avec la valeur true ;

- on met l'utilisateur à jour dans la table avec la méthode update.

On continuera à explorer cette application dans les prochains chapitres mais n'hésitez pas à fouiller dans le code !

IV-H - En résumé

- On peut créer des enregistrements avec Eloquent avec la méthode create.

- On peut modifier des enregistrements avec Eloquent avec la méthode update.

- On peut créer ou modifier des enregistrements avec Eloquent avec la méthode save.

- Le résultat d'une requête générée avec Eloquent est soit un modèle soit une collection.

- Eloquent est associé à un puissant générateur de requêtes (Query Builder).

- Des méthodes spéciales permettent de faire plusieurs actions comme aller chercher un enregistrement et le créer s'il n'existe pas.

V - 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.

Il vous faut l'application d'exemple installée et fonctionnelle pour ce chapitre parce que nous allons considérer deux de ses tables et donc de ses modèles. On a les tables users et posts, la première mémorise les utilisateurs et la seconde les articles. Il y a un lien évident entre les deux parce que ce sont des utilisateurs qui rédigent des articles.

V-A - Les migrations

V-A-1 - La table users

Voici la migration pour la table users :

1. <?php

3. use Illuminate\Database\Migrations\Migration;

4. use Illuminate\Database\Schema\Blueprint;

6. class CreateUsersTable extends Migration {

8. public function up()

9. {

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

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

12. $table->timestamps();

13. $table->string('name')->unique();

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

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

16. $table->rememberToken();

17. $table->enum('role', array('user', 'redac', 'admin'));

18. $table->boolean('valid')->default(false);

19. });

20. }

22. public function down()

23. {

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

25. }

26. }

On retrouve la trame de base de la migration d'origine de Laravel avec ces ajouts :

$table->enum('role', array('user', 'redac', 'admin'));
$table->boolean('valid')->default(false);

La colonne role de type enum a pour but de définir le rôle de l'utilisateur qui peut être :

- administrateur : admin (toutes les autorisations) ;

- rédacteur : redac (peut rédiger des articles) ;

- utilisateur : user (peut rédiger des commentaires).

La colonne valid est de type booléen et sert à définir si un utilisateur est correct pour les commentaires. Autrement dit le premier commentaire d'un utilisateur doit être validé par un administrateur, ce qui a pour effet de mettre cette colonne à true. Les commentaires suivants sont directement visibles.

Si vous regardez la table dans la base vous trouverez deux autres colonnes : confirmed et confirmation_code. Ces deux colonnes sont générées par une migration du package laravel-email-confirmation qui se trouve dans le vendor.

V-A-2 - La table posts

Voici la migration de la table posts :

1. <?php

3. use Illuminate\Database\Migrations\Migration;

4. use Illuminate\Database\Schema\Blueprint;

6. class CreatePostsTable extends Migration {

8. public function up()

9. {

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

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

12. $table->timestamps();

13. $table->string('title');

14. $table->string('slug')->unique();

15. $table->string('seo_title')->nullable();

16. $table->text('excerpt');

17. $table->text('body');

18. $table->text('meta_description');

19. $table->text('meta_keywords');

20. $table->boolean('active')->default(false);

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

22. $table->string('image')->nullable();

23. });

24. }

26. public function down()

27. {

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

29. }

30. }

Il y a de nombreuses colonnes :

- l'identifiant ;

- les dates ;

- le titre ;

- le slug ;

- le titre SEO ;

- le texte d'introduction (excerpt) ;

- le corps du texte (body) ;

- les metas (meta_description et meta_keywords) ;

- le statut (active) ;

- l'URL de l'image (image).

Et la colonne qui va avoir toute son importance pour ce chapitre : user_id, autrement dit l'identifiant de l'utilisateur qui a rédigé l'article. C'est ainsi que les deux tables vont être en relation.

V-A-3 - La migration des clés étrangères

La définition des clés étrangères sont toutes dans le fichier database/migrations/ 2017_03_18_145916_create_foreign_keys.php. Voici la partie qui nous intéresse :

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

2. $table->foreign('user_id')->references('id')->on('users')

3. ->onDelete('restrict')

4. ->onUpdate('restrict');

5. });

Dans la table posts on déclare une clé étrangère (foreign) nommée user_id qui référence (references) la colonne 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.

V-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 et on l'a vu dans les migrations : on prévoit dans la table posts une colonne 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 posts_users_id_foreign dessinée entre la clé id dans la table users et la clé étrangère user_id dans la table posts.

V-C - Les modèles et la relation

V-C-1 - Le modèle User

Le modèle User se trouve ici :

Il comporte en particulier ce code :

1. public function posts()

2. {

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

4. }

On déclare ici avec la méthode posts (au pluriel) qu'un utilisateur a plusieurs (hasMany) articles (Post). On aura ainsi une méthode pratique pour récupérer les articles d'un utilisateur.

V-C-2 - Le modèle Post

Le modèle Post se trouve ici :

Il comporte en particulier ce code :

1. public function user()

2. {

3. return $this->belongsTo(User::class);

4. }

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.

V-C-3 - La relation 1:n

Voici une schématisation de la 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\Models\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\Models\Post::find(1)->user;

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

V-D - Jouer avec la relation

On va voir maintenant ce qu'on peut faire avec cette relation…

Pour changer un peu on ne va pas utiliser Tinker mais entrer directement du code dans le contrôleur Font/PostController, plus exactement dans la méthode index :

1. public function index()

2. {

3. $posts = $this->postRepository->getActiveOrderByDate($this->nbrPages);

5. return view('front.index', compact('posts'));

6. }

Vous allez commenter les deux lignes de code et ça va constituer notre terrain de jeu. C'est la méthode qui est appelée avec la route de base de l'application.

Entrez ce code à la place :

1. $users = \App\Models\User::take(2)->get();

3. foreach($users as $user) {

4. echo '<strong>' . $user->name . '</strong><br>';

5. foreach($user->posts as $post) {

6. echo $post->title . '<br>';

7. }

8. }

En lançant l'application vous devez obtenir ceci :

1. GreatAdmin

2. Post 1

3. Post 2

4. GreatRedactor

5. Post 3

6. Post 4

7. Post 5

8. Post 6

9. Post 7

10. Post 8

11. Post 9

12. Post 10

Remarquez la méthode take qui permet de limiter le nombre d'enregistrements retournés.

On obtient le nom des deux premiers utilisateurs et la liste des titres des articles qu'ils ont rédigés. On utilise la relation avec ce code :

foreach($user->posts as $post) {

Autrement dit pour chaque utilisateur on va aller chercher les articles qu'il a écrits, donc on a un accès à la base. Regardons les requêtes générées avec la barre de débogage :

select * from `users` limit 2
select * from `posts` where `posts`.`user_id` = '1' and `posts`.`user_id` is not null
select * from `posts` where `posts`.`user_id` = '2' and `posts`.`user_id` is not null

Si on a beaucoup d'utilisateurs à afficher ça va devenir gênant quand au nombre d'accès à la base mais Laravel a une solution !

V-D-1 - L'eager loading

Dans l'exemple qu'on a vu ci-dessus on parle de lazy loading, de chargement paresseux, parce qu'on accède à la base au moment où on en a besoin. Mais la bonne idée serait plutôt d'anticiper et de tout charger avec une seule requête et ensuite d'itérer dans la collection. Voici la solution :

$users = \App\Models\User::with('posts')->take(2)->get();

On charge les utilisateurs avec (with) les articles (posts). On ne voit le changement qu'au niveau des requêtes :

select * from `users` limit 2
select * from `posts` where `posts`.`user_id` in ('1', '2')

Cette fois on n'en a que deux. La deuxième charge les articles des deux utilisateurs sélectionnés.

On peut aussi ajouter une contrainte sur la relation chargée avec une closure, par exemple :

$users = \App\Models\User::with(['posts' => function ($query) {
$query->latest('title');
}])->take(2)->get();

Ici on impose un classement aux articles :

1. GreatAdmin

2. Post 2

3. Post 1

4. GreatRedactor

5. Post 9

6. Post 8

7. Post 7

8. Post 6

9. Post 5

10. Post 4

11. Post 3

12. Post 10

V-D-2 - Tester la présence de la relation

Parfois on a envie de savoir s'il y a des enregistrements en relation. Par exemple dans notre cas si les utilisateurs ont écrit un article. Voici la syntaxe à utiliser avec la méthode has :

1. $users = \App\Models\User::has('posts')->get();

3. foreach($users as $user) {

4. echo $user->name . '<br>';

5. }

Avec ce résultat :

GreatAdmin
GreatRedactor

Ce sont les seuls utilisateurs à avoir écrit au moins un article.

On peut avoir plus de latitude dans ce filtrage en utilisant une closure avec la méthode whereHas :

$users = \App\Models\User::whereHas('posts', function ($query) {
$query->where('title', 'like', '%5%');
})->get()

Là on n'a plus qu'un seul utilisateur :

GreatRedactor

On a les réciproques de ces deux méthodes avec doesntHave (pour has) et whereDoesntHave (pour whereHas).

V-D-3 - Compter les enregistrements en relation

On peut aussi facilement compter les enregistrements en relation avec la méthode withCount :

$user = \App\Models\User::withCount('posts')->find(1);

echo $user->name . ' a écrit ' . $user->posts_count . ' articles.';

Avec ce résultat :

GreatAdmin a écrit 2 articles.

On peut ajouter des contraintes avec une closure si on en a besoin, de la même manière qu'on a vue avec with.

V-E - L'application d'exemple

V-E-1 - Affichage d'un article

Dans l'application d'exemple on n'utilise pas la relation dans le sens User vers (hasMany) Post. Il n'y a aucune page où on liste les articles par utilisateur. Par contre la relation inverse, dans le sens Post vers (belongsTo) User est utilisée. Lorsqu'on sélectionne un article dans la page d'accueil pour l'afficher on a besoin d'aller chercher les informations de l'article ainsi que celles de l'utilisateur qui l'a rédigé. On a besoin aussi d'un certain nombre d'autres informations comme les commentaires, les tags, les catégories, mais nous verrons ces éléments-là dans d'autres chapitres. Le nom de l'auteur figure en bas de l'article :

Les articles sont sélectionnés par leur slug. Par exemple le huitième article a comme URL …/posts/post-8. Le slug est unique pour chaque article et doit normalement évoquer de façon sémantique son contenu et être facile à digérer pour les moteurs de recherche. Comme il figure dans l'URL il ne doit comporter aucun caractère « interdit ».

Dans le fichier PostRepository on a la fonction getPostBySlug qui a justement pour mission de récupérer toutes les informations nécessaires à l'affichage d'un article à partir de son slug :

1. public function getPostBySlug($slug)

2. {

3. // Post for slug with user, tags and categories

4. $post = $this->model->with([

5. 'user' => function ($q) {

6. $q->select('id', 'name', 'email');

7. },

8. 'tags' => function ($q) {

9. $q->select('tags.id', 'tag');

10. },

11. 'categories' => function ($q) {

12. $q->select('title', 'slug');

13. }

14. ])

15. ->with(['parentComments' => function ($q) {

16. $q->with('user')

17. ->latest()

18. ->take(config('app.numberParentComments'));

19. }])

20. ->withCount('validComments')

21. ->withCount('parentComments')

22. ->whereSlug($slug)

23. ->firstOrFail();

25. // Previous post

26. $post->previous = $this->getPreviousPost($post->id);

28. // Next post

29. $post->next = $this->getNextPost($post->id);

31. return compact('post');

32. }

On peut remarquer le chargement des informations de l'utilisateur (eager loading) :

1. $post = $this->model->with([

2. 'user' => function ($q) {

3. $q->select('id', 'name', 'email');

On a une contrainte concernant les colonnes retournées : on ne prend que les trois qui nous intéressent. Si vous regardez dans la barre de débogage vous trouverez cette requête :

select `id`, `name`, `email` from `users` where `users`.`id` in ('2')

D'autre part avec ce code :

->whereSlug($slug)
->firstOrFail();

On fait la recherche par le slug. On a une formulation « magique » équivalente à celle-ci :

->where('slug', $slug)

La méthode firstOrFail est équivalente à la méthode first à la différence que si aucun enregistrement n'est trouvé on aura la génération d'une erreur 404.

La route pour l'affichage d'un article est là :

1. Route::prefix('posts')->namespace('Front')->group(function () {

2. Route::name('posts.display')->get('{slug}', 'PostController@show');

3. ...

4. });

Quelques remarques sur la syntaxe de ce groupe de routes :

- prefix : toutes les routes seront préfixées avec posts ;

- namespace : les noms des contrôleurs seront dans cet espace de nom (Front) sans avoir besoin de le préciser.

La route apparaît ainsi avec Artisan :

On arrive donc sur la méthode show du contrôleur Front/PostController :

1. public function show(Request $request, $slug)

2. {

3. $user = $request->user();

5. return view('front.post', array_merge($this->postRepository->getPostBySlug($slug), compact('user')));

6. }

On voit l'appel à la méthode du repository qu'on a vue plus haut (getPostBySlug). On transmet également l'utilisateur en cours de visite ($request->user()), mais c'est pour les commentaires et on verra ça dans un autre chapitre.

V-E-2 - La liste des articles dans l'administration

Un administrateur ou un rédacteur (mais uniquement pour ses propres articles) peut avoir la liste des articles dans l'administration :

Dans le repository PostRepository on a la fonction getAll qui, comme son nom l'indique, doit récupérer tous les articles (mais quand même paginés) :

1. public function getAll($nbrPages, $parameters)

2. {

3. return $this->model->with ('ingoing')

4. ->orderBy ($parameters['order'], $parameters['direction'])

5. ->when ($parameters['active'], function ($query) {

6. $query->whereActive (true);

7. })->when ($parameters['new'], function ($query) {

8. $query->has ('ingoing');

9. })->when (auth()->user()->role === 'redac', function ($query) {

10. $query->whereHas('user', function ($query) {

11. $query->where('users.id', auth()->id());

12. });

13. })->paginate ($nbrPages);

14. }

Voici la version épurée pour ne garder que ce qui nous intéresse pour ce chapitre :

1. return $this->model->with ('ingoing')

2. ...

3. })->when (auth()->user()->role === 'redac', function ($query) {

4. $query->whereHas('user', function ($query) {

5. $query->where('users.id', auth()->id());

6. });

7. ...

La méthode when est très pratique parce qu'elle permet d'ajouter des éléments à une requête selon la situation. Ici on dit que quand (when) le rôle de l'utilisateur actuel auth()->user()->role est redac (donc c'est un rédacteur) alors on filtre les articles avec whereHas sur la relation avec users, en ne gardant que les siens : $query->where('users.id', auth()->id()).

En effet on veut qu'un rédacteur n'ait accès qu'à ses propres articles dans la partie administration. On verra qu'il faut prendre d'autres précautions plus tard.

Si vous cherchez comment cette fonction est appelée à partir du contrôle Back/PostController vous allez un peu vous promener parce que la méthode index concernée est dans le trait Indexable :

1. public function index(Request $request)

2. {

3. $parameters = $this->getParameters ($request);

5. // Get records and generate links for pagination

6. $records = $this->repository->getAll (config ("app.nbrPages.back.$this->table"), $parameters);

7. $links = $records->appends ($parameters)->links ('back.pagination');

9. // Ajax response

10. if ($request->ajax ()) {

11. return response ()->json ([

12. 'table' => view ("back.$this->table.table", [$this->table => $records])->render (),

13. 'pagination' => $links->toHtml (),

14. ]);

15. }

17. return view ("back.$this->table.index", [$this->table => $records, 'links' => $links]);

18. }

En effet cette méthode est commune à plusieurs contrôleurs et l'affichage des articles dépend de la sélection des nouveaux articles ou des articles actifs :

Par exemple avec l'URL …/admin/posts?new=on on veut seulement les nouveaux articles. C'est pour cette raison que dans la méthode index on récupère les paramètres de la requête HTTP et qu'on les transmet au repository :

$records = $this->repository->getAll (config ("app.nbrPages.back.$this->table"), $parameters);

Dans le repository on en tient compte pour construire la requête :

1. ->when ($parameters['active'], function ($query) {

2. $query->whereActive (true);

3. })->when ($parameters['new'], function ($query) {

4. $query->has ('ingoing');

Mais je reparlerai de tout ça dans un prochain chapitre !

V-F - La population (seeding)

Lorsqu'on fait la population il faut faire attention lorsqu'il y a des relations. Dans le cas de la relation 1:n qu'on vient de voir dans ce chapitre il faut commencer par créer les enregistrements côté 1 (dans notre cas les users) avant ceux du côté n (dans notre cas les posts). En effet on a besoin des identifiants des utilisateurs pour renseigner la table des articles.

Ainsi dans la classe DatabaseSeeder on a d'abord la création des utilisateurs, par exemple :

1. $user = User::create(

2. [

3. 'name' => 'Martinobinus',

4. 'email' => 'martin@la.fr',

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

6. 'role' => 'user',

7. 'valid' => false,

8. 'confirmed' => false,

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

10. ]

11. );

Et ensuite la création des articles, par exemple :

1. Post::create([

2. 'title' => 'Post 9',

3. 'slug' => 'post-9',

4. 'seo_title' => 'Post 9',

5. 'user_id' => 2,

6. 'image' => '/files/user2/img09.jpg',

7. 'meta_description' => 'Aperiam molestiae ut sed vel harum nulla vel.',

8. 'meta_keywords' => 'minus,facilis,quo',

9. 'excerpt' => 'Consequatur sequi temporibus enim. Neque atque quo et rerum. Nihil quis maxime eos aut qui modi. Eos illo iste quaerat voluptatem illum.',

10. 'body' => 'Asperiores dicta necessitatibus ea. Veritatis beatae similique accusantium ad omnis. Nihil laudantium quo dolor expedita. Quia qui voluptas ipsa omnis magni et aut voluptatem. Et molestiae explicabo delectus voluptas voluptates.',

11. 'active' => true,

12. ]);

On peut alors bien renseigner la colonne user_id parce que les utilisateurs existent déjà dans la base. On verra dans un prochain chapitre qu'on peut faire quelque chose de plus efficace avec les factories.

V-G - En résumé

- Une relation de type 1:n nécessite la création d'une clé étrangère côté n.

- Une relation dans la base nécessite la mise en place de méthodes spéciales dans les modèles.

- On dispose de plusieurs méthodes pour manipuler une relation.

VI - La relation n:n

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.

VI-A - 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.

Dans l'application d'exemple nous avons deux cas où une table pivot a été ainsi mise en œuvre pour réaliser une relation de type n:n :

- entre les catégories et les articles : une catégorie peut avoir plusieurs articles et réciproquement un article peut être rangé dans plusieurs catégories ;

- entre les articles et les tags : un article peut avoir plusieurs tags et réciproquement un tag peut caractériser plusieurs articles.

VI-B - Les catégories et les articles

VI-B-1 - Présentation

Voyons le premier cas entre les catégories et les articles. Vous avez sans doute remarqué que dans le menu de la page d'accueil on a un item catégories :

Le fait de choisir une catégorie sélectionne uniquement les articles qui correspondent à cette catégorie.

D'autre part dans l'affichage d'un article on mentionne la ou les catégories :

VI-B-2 - Les données

Au niveau des tables on a ce schéma :

On a déjà vu la table posts lors du précédent chapitre. La table categories est bien plus légère puisqu'elle ne comporte que deux colonnes :

- title ;

- slug.

La relation entre les deux tables est assurée par la table pivot . Cette table pivot contient les clés des deux tables :

- category_id pour mémoriser la clé de la table categories ;

- post_id pour mémoriser la clé de la table posts.

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 (avec les règles anglaises) pris dans l'ordre alphabétique.

VI-B-3 - Les migrations

On a déjà vu dans le chapitre précédent la migration de la table posts. Celle de la table categories ne présente aucune particularité. Il est par contre intéressant d'aller voir la migration des clés étrangères dans le fichier database/migrations/ 2017_03_18_145916_create_foreign_keys.php :

1. Schema::table('category_post', function(Blueprint $table) {

2. $table->foreign('category_id')->references('id')->on('categories')

3. ->onDelete('cascade')

4. ->onUpdate('cascade');

5. });

6. Schema::table('category_post', function(Blueprint $table) {

7. $table->foreign('post_id')->references('id')->on('posts')

8. ->onDelete('cascade')

9. ->onUpdate('cascade');

10. });

On retrouve le principe de ce qu'on a déjà vu dans le précédent chapitre. Par exemple dans la table on déclare une clé étrangère (foreign) nommée category_id qui référence (references) la colonne id dans la table (on) categories. En cas de suppression (onDelete) ou de modification (onUpdate) on a une cascade.

C'est la même chose pour l'autre clé avec une cascade. Je vous ai dit lors du précédent chapitre qu'on évitait en général les cascades mais on peut aussi les utiliser intelligemment. Le tout est de bien savoir ce qu'on fait.

La cascade ici est entre les deux tables encadrantes et la table pivot, autrement dit on veut que si une catégorie disparaît (ou un article) toutes les lignes de la table pivot en correspondance disparaissent aussi, ce qui est logique. Par contre en aucun cas on a le risque de voir un article (ou une catégorie) disparaître.

VI-B-4 - Les modèles

Le modèle Category se trouve ici :

Il comporte en particulier ce code :

1. public function posts()

2. {

3. return $this->belongsToMany(Post::class);

4. }

On déclare ici avec la méthode posts (au pluriel) qu'une catégorie appartient à plusieurs (belongsToMany) articles (Post). On aura ainsi une méthode pratique pour récupérer les articles d'une catégorie.

On a déjà vu le modèle Post précédemment, on va se contenter de regarder ce code :

1. public function categories()

2. {

3. return $this->belongsToMany(Category::class);

4. }

C'est le même principe que pour les catégories puisque la relation est symétrique. On déclare avec la méthode categories (au pluriel) qu'un article appartient à plusieurs (belongsToMany) catégories (Category).

VI-B-5 - La relation n:n

Voici une schématisation de cette relation avec les deux méthodes symétriques :

VI-C - Les articles et les tags

Voyons maintenant le second cas de relation n:n de l'application, entre les articles et les tags.

VI-C-1 - Présentation

Dans l'affichage d'un article on a les tags en relation :

Ces tags sont cliquables et si on clique on se retrouve avec tous les articles qui sont en relation avec ce tag. On est prévenu par une petite barre d'information :

VI-C-2 - Les données

Au niveau des tables on a ce schéma :

La relation entre les deux tables (posts et tags) est assurée par la table pivot post_tag. Cette 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.

Je ne présente pas les migrations des clés étrangères qui sont exactement comme celles qu'on a vues pour le cas précédent.

VI-C-3 - Les modèles

Dans le modèle Post on a ce code :

1. public function tags()

2. {

3. return $this->belongsToMany(Tag::class);

4. }

C'est encore le même principe. On déclare avec la méthode tags (au pluriel) qu'un article appartient à plusieurs (belongsToMany) tags (Tag).

Dans le modèle Tag on a le code équivalent :

1. public function posts()

2. {

3. return $this->belongsToMany(Post::class);

4. }

On déclare avec la méthode posts (au pluriel) qu'un tag appartient à plusieurs (belongsToMany) articles (Post).

VI-C-4 - La relation n:n

Voici une schématisation de cette relation avec les deux méthodes symétriques :

VI-D - Jouer avec les relations

On va voir maintenant ce qu'on peut faire avec cette relation…

Comme dans le chapitre précédent on va entrer directement du code dans le contrôleur Front/PostController, plus exactement dans la méthode index :

1. public function index()

2. {

3. $posts = $this->postRepository->getActiveOrderByDate($this->nbrPages);

5. return view('front.index', compact('posts'));

6. }

Vous allez commenter les deux lignes de code et ça va constituer notre terrain de jeu. C'est la méthode qui est appelée avec la route de base de l'application.

Toutes les méthodes qu'on a vues pour la relation 1:n fonctionnent avec la relation n:n, ce qui est logique. Le fait qu'il y ait une table pivot ne change rien au fait que la relation, vue de l'une des deux tables, ressemble à s'y méprendre à une relation 1:n. Si je choisis une catégorie par exemple je sais qu'elle peut avoir plusieurs articles liés. On peut donc écrire ce code :

1. $categories = \App\Models\Category::all();

2. foreach($categories as $category) {

3. echo '<strong>' . $category->title . '</strong><br>';

4. foreach($category->posts as $post) {

5. echo $post->title . '<br>';

6. }

7. }

Et on obtient ça :

1. Category 1

2. Post 4

3. Post 7

4. Post 8

5. Post 9

6. Category 2

7. Post 3

8. Post 4

9. Post 5

10. Post 6

11. Post 7

12. Post 10

13. Category 3

14. Post 1

15. Post 2

16. Post 5

17. Post 6

18. Post 8

19. Post 10

Et on peut écrire exactement l'inverse en partant de la table des articles (je limite le nombre d'enregistrement pour ne pas surcharger l'affichage) :

1. $posts = \App\Models\Post::take(4)->get();

2. foreach($posts as $post) {

3. echo '<strong>' . $post->title . '</strong><br>';

4. foreach($post->categories as $category) {

5. echo $category->title . '<br>';

6. }

7. }

Avec ce résultat :

1. Post 1

2. Category 3

3. Post 2

4. Category 3

5. Post 3

6. Category 2

7. Post 4

8. Category 1

9. Category 2

On peut aussi faire de l'eager loading, placer des contraintes, etc. tout ce qu'on a vu lors du précédent chapitre.

VI-D-1 - Attacher et détacher

Par contre la nouveauté tient à la gestion de la table intermédiaire. Par exemple ci-dessus je vois que l'article Post 1 appartient seulement à Category 3. Si je veux lui ajouter Category 1 comment faire ? Dans ce cas on parle d'attachement : on va attacher la catégorie à l'article :

1. $post = \App\Models\Post::whereTitle('Post 1')->first();

2. $category_id = \App\Models\Category::whereTitle('Category 1')->first()->id;

4. $post->categories()->attach($category_id);

On passe à la méthode attach l'identifiant (on peut en mettre plusieurs dans un tableau) de l'enregistrement en relation et Eloquent se charge de renseigner la table pivot :

On voit qu'on a ajouté la catégorie d'identifiant 1 pour l'article d'identifiant 1. Ce qui est bien ce qu'on voulait.

De la même manière la méthode detach permet de faire l'inverse :

1. $post = \App\Models\Post::whereTitle('Post 1')->first();

2. $category_id = \App\Models\Category::whereTitle('Category 1')->first()->id;

4. $post->categories()->detach($category_id);

Et on voit dans la table que la ligne a disparu :

VI-D-2 - Synchroniser

Dans les situations « réelles » on se retrouve souvent avec quelques identifiants à attacher tout en supprimant ceux qui y sont déjà. On pourrait le faire en deux étapes : d'abord tout détacher, puis attacher les nouveaux. Mais on peut aussi synchroniser avec la méthode sync :

$post = \App\Models\Post::whereTitle('Post 1')->first();

$post->categories()->sync([2, 3]);

Avec ce résultat dans la table :

La catégorie 1 a disparu et on se retrouve avec la 2 et la 3.

Mais parfois on veut synchroniser sans détacher. Dans ce cas au lieu d'utiliser sync on utilise syncWithoutDetaching.

VI-D-3 - Échanger (toggle)

Prenons un dernier cas : je veux que si la catégorie est déjà présente elle disparaisse mais que si elle ne l'est pas elle le soit. Pas clair ? Voici un exemple avec la méthode toggle :

$post = \App\Models\Post::whereTitle('Post 1')->first();

$post->categories()->toggle([1, 3]);

Avec ce résultat :

La catégorie 3 était présente, donc on l'a retirée. La catégorie 1 n'était pas présente, alors on l'a ajoutée.

VI-E - L'application d'exemple

VI-E-1 - Enregistrement d'un article

VI-E-1-a - Présentation

Si vous vous connectez en tant qu'administrateur vous avez accès dans l'administration à la liste des articles :

Pour chaque article on a un bouton jaune qui permet d'accéder au formulaire de modification :

Il y a un cadre pour les catégories (avec une liste à choix multiple) et un autre pour les tags (avec les tags séparés par des virgules).

VI-E-1-b - Route et contrôleur

Voici la route pour la soumission de ce formulaire :

On a ces informations :

- le verbe PUT/PATCH ;

- l'URL admin/posts/{post} ;

- le nom posts.update ;

- le contrôleur App\Http\Back\PostController ;

- la méthode update du contrôleur.

Voici la méthode en question :

1. public function update(PostRequest $request, Post $post)

2. {

3. $this->authorize('manage', $post);

5. $this->repository->update($post, $request);

7. return back()->with('post-ok', __('The post has been successfully updated'));

8. }

Je vous ai déjà parlé de l'injection de dépendance, on a ici deux cas :

- la requête de formulaire pour la validation PostRequest ;

- le modèle Post.

Cette deuxième injection peut vous intriguer. En effet dans l'URL on transmet l'identifiant de l'article, alors ici on devrait recevoir un entier, pas un modèle…

Mais comme Laravel est intelligent il établit un lien implicite entre la valeur dans l'URL et le modèle Post en déduisant que la valeur correspond à un identifiant et du coup il instancie un modèle de la classe Post avec cet identifiant !

Du coup on voit qu'on peut transmettre directement au repository le modèle ($post) et les entrées du formulaire ($request).

On voit qu'après la mise à jour effectuée par le repository on renvoie la même vue (back) avec (with) un message.

VI-E-1-c - La validation

La validation s'effectue dans la requête de formulaire PostRequest dont voici les règles :

1. public function rules()

2. {

3. $regex = '/^[A-Za-z0-9-éèàù]{1,50}?(,[A-Za-z0-9-éèàù]{1,50})*$/';

4. $id = $this->post ? ',' . $this->post->id : '';

6. return $rules = [

7. 'title' => 'bail|required|max:255',

8. 'body' => 'bail|required|max:65000',

9. 'slug' => 'bail|required|max:255|unique:posts,slug' . $id,

10. 'excerpt' => 'bail|required|max:65000',

11. 'meta_description' => 'bail|required|max:65000',

12. 'meta_keywords' => 'bail|required|regex:' . $regex,

13. 'seo_title' => 'bail|required|max:255',

14. 'image' => 'bail|required|max:255',

15. 'categories' => 'required',

16. 'tags' => 'nullable|regex:' . $regex,

17. ];

18. }

Voyons de plus près la règle pour les tags :

'tags' => 'nullable|regex:' . $regex,

On applique une expression régulière :

$regex = '/^[A-Za-z0-9-éèàù]{1,50}?(,[A-Za-z0-9-éèàù]{1,50})*$/';

En effet on veut une succession de mots séparés par des virgules. On n'a pas ça dans la batterie des règles de Laravel, il faut donc s'adapter. La même expression est d'ailleurs utilisée pour les mots clés META.

Si par exemple vous insérez un espace à la soumission vous aurez une erreur :

Rappel : pour avoir l'interface en français il faut mettre la locale à fr dans le fichier config/app.php.

Mais où se trouve ce texte spécifique à cette règle ? Regardez dans le fichier resources/lang/fr/validation.php :

1. 'custom' => [

2. 'tags' => [

3. 'regex' => "Les marques, séparées par des virgules (sans espaces), doivent avoir au maximum 50 caractères.",

4. ],

5. 'meta_keywords' => [

6. 'regex' => "Les mots clés, séparés par des virgules (sans espaces), doivent avoir au maximum 50 caractères.",

7. ],

8. ],

Tous les textes personnalisés doivent être placés dans custom.

VI-E-1-d - Le repository

C'est dans le repository PostRepository qu'on va trouver la gestion des données :

1. public function update($post, $request)

2. {

3. $request->merge(['active' => $request->has('active')]);

5. $post->update($request->all());

7. $this->saveCategoriesAndTags($post, $request);

8. }

On a :

- l'ajout (merge) dans les entrées de la case à cocher (active) ;

- la mise à jour de l'enregistrement (update) à partir des entrées du formulaire ;

- la délégation à la fonction saveCategoriesAndTags pour la mise à jour des catégories et des tags.

Voyons cette dernière fonction :

1. protected function saveCategoriesAndTags($post, $request)

2. {

3. $post->categories()->sync($request->categories);

5. $tags_id = [];

7. if ($request->tags) {

8. $tags = explode(',', $request->tags);

9. foreach ($tags as $tag) {

10. $tag_ref = Tag::firstOrCreate(['tag' => $tag]);

11. $tags_id[] = $tag_ref->id;

12. }

13. }

15. $post->tags()->sync($tags_id);

16. }

On a la synchronisation (sync) des catégories.

Pour les tags c'est un peu plus délicat pour synchroniser :

- on vérifie si on a des tags : if ($request->tags) { ;

- on crée un tableau avec les tags saisis : $tags = explode(',', $request->tags) ;

- on boucle dans le tableau : foreach ($tags as $tag) { ;

- on crée au besoin le tag s'il n'existe pas (firstOrCreate) et on récupère son identifiant : $tag_ref = Tag::firstOrCreate(['tag' => $tag]) ;

- on remplit un tableau avec les identifiants : $tags_id[] = $tag_ref->id ;

- on utilise à la fin ce tableau pour synchroniser les tags de l'article : $post->tags()->sync($tags_id).

VI-E-1-e - La vue

Au niveau de la vue pour ce formulaire on la trouve ici :

Avec ce code :

1. @extends('back.posts.template')

3. @section('form-open')

4. <form method="post" action="{{ route('posts.update', [$post->id]) }}">

5. {{ method_field('PUT') }}

6. @endsection

On trouve peu de code parce que le formulaire est partagé avec la vue de création d'un article. On fait du coup appel au template back/posts/template. Dans ce template on trouve cette ligne :

@yield('form-open')

C'est ici qu'est inséré le code pour l'ouverture du formulaire qui est forcément différent pour une création ou une modification (pas la même URL).

Remarquez l'utilisation de l'helper route pour créer la valeur de l'attribut action :

route('posts.update', [$post->id])

Ce qui génère un code de ce genre :

<form method="post" action="http://monsite.dev/admin/posts/8">

Mais si vous êtes observateur vous avez vu que la route attend un verbe PUT, or là on a un POST. Alors ?

Les navigateurs ne savent tout simplement pas générer autre chose que du GET ou du POST alors il faut ruser. On utilise POST pour ne pas contrarier le navigateur mais on ajoute cette ligne :

{{ method_field('PUT') }}

Ce qui a pour effet de créer un champ caché :

<input type="hidden" name="_method" value="PUT">

À l'arrivée Laravel repère ce champ et sait qu'on veut un PUT !

Je parlerai plus en détail des vues dans un chapitre ultérieur, en particulier des vues partielles et des composants.

VI-E-2 - La population (seeding)

Pour terminer un petit mot encore sur la population. J'avais précisé pour la relation 1:n qu'il faut respecter un ordre dans la population pour disposer de la clé étrangère. Pour la relation n:n c'est également vrai et on ne peut remplir la table pivot que lorsque les enregistrements des deux tables encadrantes ont été créés.

On crée donc les articles et les tags, par exemple pour les tags on a :

1. DB::table('tags')->insert([

2. ['tag' => 'Tag1'],

3. ['tag' => 'Tag2'],

4. ['tag' => 'Tag3'],

5. ['tag' => 'Tag4'],

6. ['tag' => 'Tag5'],

7. ['tag' => 'Tag6']

8. ]);

Ici on n'utilise pas Eloquent mais directement le Query Builder en précisant le nom de la table concernée.

Les tags sont ensuite affectés de façon aléatoire aux articles (sauf pour un certain article pour une raison que nous verrons plus tard) :

1. foreach ($posts as $post) {

2. if ($post->id === 9) {

3. $numbers=[1,2,5,6];

4. $n = 4;

5. } else {

6. $numbers = range (1, $nbrTags);

7. shuffle ($numbers);

8. $n = rand (2, 4);

9. }

10. for($i = 0; $i < $n; ++$i) {

11. $post->tags()->attach($numbers[$i]);

12. }

13. }

On utilise la méthode attach qu'on a vue ci-dessus pour remplir la table pivot.

VI-F - 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 (attach, detach, sync, toggle…).‌

- On peut créer des règles de validation personnalisées avec une expression rationnelle.

VII - Le polymorphisme

Lors des deux précédents chapitres on a vu les principales relations que nous offre Eloquent : hasMany et belongsTomany. Je ne vous ai pas parlé de la relation hasOne parce que c'est juste du hasMany limité à un seul enregistrement et elle est peu utilisée. Dans tous les cas qu'on a vus on considère deux tables en relation. Dans le présent chapitre on va envisager le cas où une table peut être en relation avec plusieurs autres tables, ce qui se nomme du polymorphisme.

VII-A - Un peu de théorie

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

On a vu cette relation, en voici une schématisation pour fixer les esprits :

On a a possède un b (hasOne) ou a possède plusieurs b (hasMany).

La réciproque : b est possédé par un a (belongsTo).

VII-A-2 - La relation n:n

On a vu aussi cette relation, en voici une schématisation pour fixer les esprits :

On a a appartient à un ou plusieurs b (belongsToMany).

Et on a b appartient à un ou plusieurs a (belongsToMany).

VII-A-3 - La relation une table vers plusieurs tables

VII-A-3-a - Type de relation 1:n

Maintenant imaginons cette situation :

La table c peut être en relation soit avec la table a, soit avec la table b. Dans cette situation comment gérer une clé étrangère dans la table c ? Comment l'appeler et comment savoir avec quelle table elle est en relation ?

On voit bien qu'il va falloir une autre information : connaître sûrement la table en relation.

Puisqu'on a besoin de deux informations il nous faut deux colonnes :

On a donc deux colonnes :

- relatable_id : la clé étrangère qui mémorise l'identifiant de l'enregistrement en relation ;

- relatable_type : la classe du modèle en relation.

Voici la figure complétée avec les noms de ces relations :

- morphOne : c'est le hasOne mais issu de plusieurs tables ;

- morphMany : c'est le hasMany mais issu de plusieurs tables ;

- morphTo : c'est le belongsTo mais à destination de plusieurs tables.

VII-A-3-b - Type de relation n:n

On peut avoir le même raisonnement pour une relation de type n:n avec plusieurs tables d'un côté de la relation :

- morphToMany : c'est le belongsToMany mais issu de plusieurs tables ;

- morphedByMany : c'est le belongsToMany mais en direction de plusieurs tables.

VII-B - L'application d'exemple

Maintenant qu'on a vu la théorie passons à la pratique avec un cas dans l'application d'exemple.

VII-B-1 - Présentation

Quand vous allez dans l'administration vous tombez sur le tableau de bord :

Il y a des pavés qui indiquent le nombre de nouveaux contacts, articles, utilisateurs et commentaires. Chaque fois que l'une de ces entités est créée on le mémorise dans la base, dans la table ingoings :

Il y a deux colonnes intéressantes :

- ingoing_id ;

- ingoing_type.

Ça doit vous évoquer quelque chose par rapport à ce que j'ai expliqué ci-dessus sur le polymorphisme. Cette table ingoings est en relation polymorphique avec quatre tables :

Si vous regardez un peu le contenu de cette table :

Vous voyez que la première colonne (ingoing_id) mémorise les identifiants, et la seconde (ingoing_type) mémorise la classe du modèle correspondant.

VII-B-2 - Les modèles

Dans le modèle App\Models\Ingoing vous trouvez cette méthode :

1. public function ingoing()

2. {

3. return $this->morphTo();

4. }

Dans les modèles App\Models\Post, App\Models\User, App\Models\Contact, App\Models\Comment, vous trouvez le trait App\Models\IngoingTrait avec ce code :

1. public function ingoing()

2. {

3. return $this->morphOne(Ingoing::class, 'ingoing');

4. }

Et ça suffit pour avoir la relation fonctionnelle !

VII-B-3 - La création des ingoings

Quand un modèle est créé il y a déclenchement d'un événement, nous verrons cet aspect événementiel dans un chapitre ultérieur. Pour le moment on va juste regarder l'écouteur (listener) de cet événement App\Listeners\ModelCreated.php :

1. public function handle(EventModelCreated $event)

2. {

3. $event->model->ingoing()->save(new Ingoing);

5. ...

6. }

On connaît le modèle créé grâce à l'événement ($event->model), on utilise la méthode save sur la relation en passant une instance de Ingoing. Un nouvel enregistrement est ainsi créé dans la table ingoings avec les colonnes ingoing_id et ingoing_type parfaitement renseignées.

VII-B-4 - Les panneaux de l'administration

Pour gérer les panneaux de l'administration il existe une classe particulière PannelAdmin :

Avec ce code :

1. <?php

3. namespace App\Services;

5. class PannelAdmin

6. {

7. ...

9. public function __construct(array $infos)

10. {

11. $this->color = $infos['color'];

12. $this->icon = $infos['icon'];

13. $this->model = new $infos['model'];

14. $this->name = __($infos['name']);

15. $this->url = $infos['url'];

16. $this->nbr = $this->getNumber ();

17. }

19. protected function getNumber()

20. {

21. return $this->model->has('ingoing')->count();

22. }

23. }

Une classe toute simple qui comporte les 6 propriétés nécessaires pour les panneaux :

- couleur (color) ;

- icône (icon) ;

- modèle (model) ;

- nom (name) ;

- URL (url) ;

- nombre de nouveaux éléments (nbr).

On voit que la dernière propriété est renseignée avec la fonction getNumber, dans laquelle on trouve ce code :

return $this->model->has('ingoing')->count();

Donc on considère tous les enregistrements qui ont (has) un ingoing et on les compte (count).

Si vous regardez les requêtes générées avec la barre de débogage vous allez trouver ce genre de requête :

select count(*) as aggregate from `contacts` where exists (select * from `ingoings` where `contacts`.`id` = `ingoings`.`ingoing_id` and `ingoings`.`ingoing_type` = 'App\Models\Contact')

Voici le code dans le contrôleur :

1. public function index()

2. {

3. $pannels = [];

5. foreach (config('pannels') as $pannel) {

7. $panelAdmin = new PannelAdmin($pannel);

9. if ($panelAdmin->nbr) {

10. $pannels[] = $panelAdmin;

11. }

12. }

14. return view('back.index', compact('pannels'));

15. }

Les panneaux sont définis dans le fichier config/pannels.php :

1. <?php

3. return [

5. [

6. 'color' => 'primary',

7. 'icon' => 'envelope',

8. 'model' => \App\Models\Contact::class,

9. 'name' => 'admin.new-messages',

10. 'url' => 'admin/contacts?new=on',

11. ],

13. ...

15. ];

Dans le contrôleur on a une boucle pour itérer tous les panneaux :

foreach (config('pannels') as $pannel) {

Et on ne crée le panneau que s'il y a au moins un nouvel enregistrement :

if ($panelAdmin->nbr) {
$pannels[] = $panelAdmin;
}

On envoie ensuite les panneaux à la vue :

return view('back.index', compact('pannels'));

Cette vue resources/views/back/index.blade.php est assez légère :

1. @extends('back.layout')

3. @section('main')

4. @admin

5. <div class="row">

6. @each('back/partials/pannel', $pannels, 'pannel')

7. </div>

8. @endadmin

9. @endsection

Je ne vais pas entrer dans le détail de la syntaxe parce que je parlerai plus longuement des vues ultérieurement mais la directive @each de Blade permet de faire une itération, on appelle donc la vue partielle …back/partials/pannel.blade.php pour chaque panneau. C'est la vue partielle qui a le code pour l'affichage :

1. <div class="col-lg-3 col-xs-6">

2. <!-- small box -->

3. <div class="small-box bg-{{ $pannel->color }}">

4. <div class="inner">

5. <h3>{{ $pannel->nbr }}</h3>

7. <p>{{ $pannel->name }}</p>

8. </div>

9. <div class="icon">

10. <span class="fa fa-{{ $pannel->icon }}"></span>

11. </div>

12. <a href="{{ $pannel->url }}" class="small-box-footer">

13. @lang('More info')

14. <span class="fa fa-arrow-circle-right"></span>

15. </a>

16. </div>

17. </div>

VII-B-5 - Suppression des ingoings

Dans l'administration quand on affiche la liste d'une entité, par exemple les contacts, on a une case à cocher pour signaler si l'entité est nouvelle ou pas :

Si on décoche ça envoie une requête en Ajax pour la mise à jour dans la base. Je ne vais pas entrer dans le détail du JavaScript mais juste montrer la méthode du contrôleur, ici Back/Contact/Controller :

1. public function updateSeen(Contact $contact)

2. {

3. $contact->ingoing->delete ();

5. return response ()->json ();

6. }

On utilise la méthode delete sur la relation (ingoing) et c'est fait ! Ensuite on renvoie une réponse vide au format JSON puisqu'on est en Ajax.

D'ailleurs si on regarde la méthode du ContactRepository pour afficher les contacts dans l'administration :

1. public function getAll($nbrPages, $parameters)

2. {

3. return Contact::with ('ingoing')

4. ->latest()

5. ->when ($parameters['new'], function ($query) {

6. $query->has ('ingoing');

7. })->paginate($nbrPages);

8. }

On voit qu'on fait un chargement (with) de l'ingoing. D'autre part quand le paramètre new est présent dans l'URL on ajoute à la requête le filtrage des ingoing ($query->has ('ingoing')) parce qu'on ne veut que les nouveaux contacts, ce qui donne ce genre de requête :

select * from `contacts` where exists (select * from `ingoings` where `contacts`.`id` = `ingoings`.`ingoing_id` and `ingoings`.`ingoing_type` = 'App\Models\Contact') order by `created_at` desc limit 3 offset 0

VII-C - En résumé

- Lorsque plusieurs tables sont concernées d'un côté d'une relation on doit appliquer le polymorphisme.

- Laravel propose de nombreuses méthodes pour gérer le polymorphisme selon la situation.

VIII - Gérer un arbre

On a souvent le cas d'une structure arborescente dans une base de données, par exemple pour un menu, des catégories et sous-catégories. Il existe deux grandes façons d'aborder et résoudre ce problème :

- on peut se contenter de prévoir une colonne pour mémoriser l'identifiant du parent. C'est simple mais contraint à une approche récursive pour retrouver les données ;

- on peut approcher ce problème sous forme d'ensembles imbriqués, c'est plus complexe mais bien plus performant (pour une explication en français c'est ici).

Dans un premier temps j'avais adopté la première approche puis je suis passé à la seconde parce que je n'étais pas satisfait du résultat en matière de performances.

Plutôt que de coder tout ça je me suis appuyé sur un package déjà tout prêt. J'ai un peu hésité parce qu'il ne semble pas être maintenu : la dernière modification remonte à 3 ans. D'ailleurs les commandes Artisan ne sont pas toutes fonctionnelles. Mais le jeu en vaut quand même la chandelle comme vous allez le voir dans cet article.

J'ai utilisé ce package pour la gestion des commentaires sur les articles.

VIII-A - Aspect fonctionnel

Dans l'application d'exemple les utilisateurs inscrits peuvent laisser des commentaires :

On peut commenter un commentaire et ainsi de suite. On a donc une organisation hiérarchique qui en plus correspond à la même entité.

Le niveau d'imbrication est réglable dans l'administration :

VIII-B - Installer un package

L'installation d'un package dans Laravel est en général très simple et se limite le plus souvent à informer composer. Pour le cas de etrepat/baum il suffit de taper :

composer require baum/baum

Et le package se charge dans le dossier vendor. D'autre part le fichier composer.json est modifié :

1. "require": {

2. ...

3. "baum/baum": "^1.1",

4. ...

5. },

Il faut ensuite informer Laravel que le package existe dans le fichier config/app.php :

1. 'providers' => [

3. ...

5. /*

6. * Package Service Providers...

7. */

8. Baum\Providers\BaumServiceProvider::class,

Cette étape est maintenant devenue inutile pour les packages prévus pour Laravel 5.5.

Selon les packages il est ensuite parfois nécessaire de lancer d'autres opérations pour publier une configuration spécifique, des vues… Ce n'est pas le cas avec notre package.

VIII-C - Les données

VIII-C-1 - Les migrations

Pour fonctionner notre package a besoin que la table concernée comporte 4 colonnes particulières. Il faut donc les prévoir dans la migration :

1. class CreateCommentsTable extends Migration {

3. public function up()

4. {

5. Schema::create('comments', function(Blueprint $table) {

6. ...

7. $table->integer('parent_id')->nullable()->index();

8. $table->integer('lft')->nullable()->index();

9. $table->integer('rgt')->nullable()->index();

10. $table->integer('depth')->nullable();

11. ...

12. });

13. }

On va donc les retrouver dans la table :

VIII-C-2 - Les modèles

Le modèle pour les commentaires ne doit pas étendre la classe Illuminate\Database\Eloquent\Model mais la classe du package Baum/Node :

use Baum\Node;

class Comment extends Node

VIII-C-3 - Les relations

La table des commentaires (comments) est en relation avec trois autres tables :

- posts : une relation 1:n (clé étrangère : post_id) parce que les articles peuvent avoir plusieurs commentaires ;

- users : une relation 1:n (clé étrangère : user_id) parce que les utilisateurs peuvent écrire plusieurs commentaires ;

- comments : une relation 1:n avec elle-même (clé étrangère : parent_id) parce que les commentaires peuvent avoir plusieurs commentaires enfants.

Peut-être cela vous semble-t-il un peu étrange cette relation réflexive des commentaires sur eux-même mais ça fonctionne.

D'autre part on pourrait à partir de cette situation établir d'autres relations. La table comments peut très bien servir de pivot entre les tables users et posts et on pourrait établir des relations belongsToMany. Mais on n'en a pas besoin parce qu'il n'est pas vraiment utile de connaître pour un utilisateur tous les commentaires qu'il a écrits ou pour un article tous les utilisateurs qui l’ont commenté. Mais rien n'empêche d'établir plusieurs sortes de relations sur une même table, Eloquent permet cette souplesse. Une relation ne sert que lorsqu'elle est utilisée.

Si vous regardez dans le modèle Post vous trouvez ce code :

1. /**

2. * One to Many relation

3. *

4. * @return \Illuminate\Database\Eloquent\Relations\HasMany

5. */

6. public function comments()

7. {

8. return $this->hasMany(Comment::class);

9. }

11. /**

12. * One to Many relation

13. *

14. * @return \Illuminate\Database\Eloquent\Relations\hasMany

15. */

16. public function validComments()

17. {

18. return $this->comments()->whereHas('user', function ($query) {

19. $query->whereValid(true);

20. });

21. }

23. /**

24. * One to Many relation

25. *

26. * @return \Illuminate\Database\Eloquent\Relations\hasMany

27. */

28. public function parentComments()

29. {

30. return $this->validComments()->whereParentId(null);

31. }

Voyons ça de plus près :

- comments : c'est la relation hasMany classique qu'on a déjà vue ;

- validComments : c'est la relation hasMany complétée par une contrainte. En effet lorsqu'un utilisateur commente la première fois il doit être modéré, il n'est pas encore valide, il y a une colonne booléenne valid dans la table users. Ici on vérifie donc que l'utilisateur est valide parce qu'on ne doit pas encore afficher ses commentaires ;

- parentComments : on veut les commentaires valides mais seulement ceux de premier niveau (qui ne sont donc pas un commentaire de commentaire), on teste juste que la clé étrangère a la valeur nulle, qu'il n'y a donc pas de parent.

Dans les deux autres modèles on a des relations classiques, belongsTo dans Comment et hasMany dans User.

VIII-D - Création d'un commentaire

VIII-D-1 - Les routes

Si vous regardez dans le fichier des routes routes/web.php vous trouvez ces deux routes :

1. // Posts and comments

2. Route::prefix('posts')->namespace('Front')->group(function () {

3. ...

4. Route::name('posts.comments.store')->post('{post}/comments', 'CommentController@store');

5. Route::name('posts.comments.comments.store')->post('{post}/comments/{comment}/comments', 'CommentController@store');

6. ...

7. });

Vous remarquez que les routes sont dans un groupe pour mutualiser :

- le préfixe post ;

- l'espace de noms Front pour les contrôleurs.

Les deux routes correspondent à ces URL :

1. post/{post}/comments

2. post/{post}/comments/{comment}/comments

Pourquoi deux routes pour créer les commentaires ?

Parce qu'on a deux cas à traiter :

1 - Le cas du commentaire parent créé avec le formulaire :

Dans ce cas on a une soumission classique du formulaire et on transmet dans l'URL l'identifiant de l'article avec le paramètre {post}.

2 - Le cas de la réponse à un commentaire existant :

Dans ce cas la soumission est faite en Ajax, pour éviter de recharger toute la page, et on transmet dans l'URL l'identifiant du commentaire parent avec le paramètre {comment} en plus de celui de l'article.

VIII-D-2 - Le contrôleur

La création d'un commentaire est traité dans la méthode store du contrôleur Front/CommentController :

1. /**

2. * Store a newly created comment in storage.

3. *

4. * @param  \App\http\requests\CommentRequest $request

5. * @param  \App\Models\Post  $post

6. * @param  integer $comment_id

7. * @return \Illuminate\Http\Response

8. */

9. public function store(CommentRequest $request, Post $post, $comment_id = null)

10. {

11. Comment::create ([

12. 'body' => $request->input('message' . $comment_id),

13. 'post_id' => $post->id,

14. 'user_id' => $request->user()->id,

15. 'parent_id' => $comment_id,

16. ]);

18. ...

20. if (!$request->user()->valid) {

21. $request->session()->flash('warning', __('Thanks for your comment. It will appear when an administrator has validated it.<br>Once you are validated your other comments immediately appear.'));

22. }

24. if($request->ajax()) {

25. return response()->json();

26. }

28. return back();

29. }

Regardez la signature de la méthode :

public function store(CommentRequest $request, Post $post, $comment_id = null)

On a 3 paramètres :

- $request : injection de la requête de formulaire pour la validation comme on l'a déjà vu ;

- $post : liaison implicite entre le paramètre {post} et le modèle Post, je rappelle qu'ainsi Laravel injecte directement une instance du modèle avec l'identifiant $post ;

- $comment_id : récupère le paramètre {comment} avec l'identifiant du commentaire parent ou rien du tout dans le cas d'un commentaire de premier niveau et dans ce cas on a la valeur par défaut null.

Dans la méthode on crée le commentaire avec la méthode store :

1. Comment::create ([

2. 'body' => $request->input('message' . $comment_id),

3. 'post_id' => $post->id,

4. 'user_id' => $request->user()->id,

5. 'parent_id' => $comment_id,

6. ]);

Si l'utilisateur n'est pas valide (c'est son premier commentaire) on lui envoie un message :

if (!$request->user()->valid) {
$request->session()->flash('warning', __('Thanks for your comment. It will appear when an administrator has validated it.<br>Once you are validated your other comments immediately appear.'));
}

Remarquez comment on peut récupérer une instance du modèle de l'utilisateur en cours avec $request->user(). D'autre part on envoie le texte du message en session valable juste pour une requête HTTP (flash). Le texte est de base en anglais, je parlerai dans un chapitre ultérieur de la localisation.

Ensuite, selon que la requête est en Ajax ou pas, on envoie la réponse adaptée :

1. if($request->ajax()) {

2. return response()->json();

3. }

5. return back();

VIII-E - Modification d'un commentaire

Dans l'application le rédacteur d'un commentaire peut ensuite le modifier. Il dispose d'un lien en forme d'icône représentant un stylo :

Ça ouvre un formulaire avec le texte actuel :

La soumission se fait en Ajax pour éviter de recharger la page. C'est la méthode update du contrôleur Front/CommentController qui gère cette modification :

1. public function update(CommentRequest $request, Comment $comment)

2. {

3. ...

5. $message = $request->input('message' . $comment->id);

6. $comment->body = $message;

7. $comment->save();

9. return ['id' => $comment->id, 'message' => $message];

10. }

On a :

- une requête de formulaire pour la validation (CommentRequest) ;

- une liaison implicite entre le paramètre {comment} et le modèle Comment.

La mise à jour se fait classiquement avec la méthode save. On retourne un tableau avec l'identifiant et le texte que Laravel transforme automatiquement en réponse JSON.

VIII-F - Suppression d'un commentaire

Dans l'application le rédacteur d'un commentaire peut ensuite le supprimer s'il a des remords. Il dispose d'un lien en forme d'icône représentant une poubelle :

On a dans la page un formulaire classique caché :

1. <form action="http://monsite.fr/comments/16" method="POST" class="hide">

2. <input type="hidden" name="_token" value="EK86XinyEke12ERSRx5Ddfu8hD6iNBBUIzUSe34y">

3. <input type="hidden" name="_method" value="DELETE">

4. </form>

C'est la méthode destroy du contrôleur Front/CommentController qui gère cette suppression :

1. public function destroy(Comment $comment)

2. {

3. ...

5. $comment->delete();

7. return back();

8. }

On a une liaison implicite entre le paramètre {comment} et le modèle Comment.

La suppression se fait classiquement avec la méthode delete.

VIII-G - Affichage des commentaires

L'affichage des commentaires se fait lorsqu'on affiche un article. C'est géré par la méthode show du contrôleur Front/PostController :

1. public function show(Request $request, $slug)

2. {

3. $user = $request->user();

5. return view('front.post', array_merge($this->postRepository->getPostBySlug($slug), compact('user')));

6. }

On récupère l'utilisateur en cours avec $request->user() et les informations nécessaires dans le repository PostRepository (méthode getPostBySlug). On a déjà commencé à voir cette méthode dans le chapitre sur la relation 1:n :

1. public function getPostBySlug($slug)

2. {

3. // Post for slug with user, tags and categories

4. $post = $this->model->with([

5. 'user' => function ($q) {

6. $q->select('id', 'name', 'email');

7. },

8. 'tags' => function ($q) {

9. $q->select('tags.id', 'tag');

10. },

11. 'categories' => function ($q) {

12. $q->select('title', 'slug');

13. }

14. ])

15. ->with(['parentComments' => function ($q) {

16. $q->with('user')

17. ->latest()

18. ->take(config('app.numberParentComments'));

19. }])

20. ->withCount('validComments')

21. ->withCount('parentComments')

22. ->whereSlug($slug)

23. ->firstOrFail();

25. // Previous post

26. $post->previous = $this->getPreviousPost($post->id);

28. // Next post

29. $post->next = $this->getNextPost($post->id);

31. return compact('post');

32. }

On va s'intéresser cette fois à cette partie du code :

1. ->with(['parentComments' => function ($q) {

2. $q->with('user')

3. ->latest()

4. ->take(config('app.numberParentComments'));

5. }])

6. ->withCount('validComments')

7. ->withCount('parentComments')

On charge (with) les commentaires de niveau un (parentComments) ainsi que les rédacteurs des commentaires (with('user')), à partir du plus récent (latest) et on ne prend (take) que ce qui est indiqué dans la configuration (app.numberParentComments).

D'autre part on récupère aussi le nombre de commentaires valides (->withCount('validComments')) et le nombre de commentaires de niveau un (->withCount('parentComments')).

Dans la vue front/post.blade.php on a ce code :

1. @if ($post->valid_comments_count)

2. ...

3. <ol class="commentlist">

4. @include('front/comments/comments', ['comments' => $post->parentComments])

5. </ol>

6. ...

7. @endif

Si on a des commentaires valides on inclut (@include) la vue front/comments/comments :

@foreach($comments as $comment)
@include('front/comments/comments-base')
@endforeach

Dans cette vue pour chaque (@foreach) commentaire on inclut la vue front/comments/comments-base. Je ne vais pas analyser tout le code mais me focaliser sur une partie intéressante :

1. @unless ($comment->isLeaf())

2. ...

3. <ul class="children">

4. @include('front/comments/comments', ['comments' => $comment->getImmediateDescendants()])

5. </ul>

6. @endunless

À moins que (@unless) le commentaire n'ait plus d'enfant ($comment->isLeaf()) on inclut à nouveau la vue front/comments/comments en passant en paramètre tous les commentaires immédiatement descendants ($comment->getImmediateDescendants()). On itère donc dans tous les commentaires pour les afficher de façon hiérarchique.

La méthode getImmediateDescendants fait partie des nombreuses méthodes disponibles avec le package pour questionner les nœuds.

La méthode isLeaf fait partie des nombreuses méthodes disponibles avec le package pour accéder aux nœuds.

VIII-H - Afficher plus de commentaires

On a vu que dans la configuration (config/app.php) on limite le nombre de commentaires de premier niveau à afficher, par défaut la valeur est 2 :

'numberParentComments' => 2,

S'il y a encore des commentaires à afficher on présente un bouton :

On trouve ce code dans la vue front/post.blade.php pour gérer l'apparition conditionnelle du bouton :

1. @if ($post->parent_comments_count > config('app.numberParentComments'))

2. <p id="morebutton" class="text-center">

3. <a id="nextcomments" href="{{ route('posts.comments', [$post->id, 1]) }}" class="button">@lang('More comments')</a>

4. </p>

5. ...

6. @endif

La requête passe en Ajax pour éviter de recharger la page. Elle est gérée par la méthode comments du contrôleur Front/CommentController :

1. public function comments(Post $post, $page)

2. {

3. $comments = $this->commentRepository->getNextComments($post, $page);

4. $count = $post->parentComments()->count();

5. $level = 0;

7. return [

8. 'html' => view('front/comments/comments', compact('post', 'comments', 'level'))->render(),

9. 'href' => $count <= config('app.numberParentComments') * ++$page ?

10. 'none'

11. : route('posts.comments', [$post->id, $page]),

12. ];

13. }

C'est le repository CommentRepository qui est chargé de récupérer les commentaires :

1. public function getNextComments(Post $post, $page)

2. {

3. return $post->parentComments()

4. ->with('user')

5. ->latest()

6. ->skip($page * config('app.numberParentComments'))

7. ->take(config('app.numberParentComments'))

8. ->get();

9. }

On prend :

- les commentaires de niveau un (parentComments) ;

- en chargeant (with) les rédacteurs (user) ;

- en commençant par les plus récents (latest) ;

- en sautant (skip) ceux qui sont déjà affichés ;

- en prenant (take) la quantité définie dans la configuration.

VIII-I - La population (seeding)

Je ne vous ai pas encore parlé des factories… Les factories sont des classes principalement consacrées aux tests mais qui peuvent aussi servir en d'autres occasions comme ici pour créer des enregistrements. On les trouve ici :

Par défaut on a juste la classe factory pour le modèle User :

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

2. static $password;

4. return [

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

6. 'email' => $faker->unique()->safeEmail,

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

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

9. ];

10. });

Sans entrer dans les détails, on utilise le composant Faker pour générer des données aléatoires pour les propriétés du modèle User. Par exemple avec ce code :

factory(User::class, 15)->create();

On crée directement quinze utilisateurs !

Voici la classe factory pour les commentaires (CommentFactory) :

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

3. return [

4. 'body' => $faker->paragraph($nbSentences = 4, $variableNbSentences = true),

5. ];

6. });

Ensuite les commentaires peuvent facilement être créés en ajoutant les informations manquantes dans la classe factory (ou en les surchargeant) :

1. $comment1 = factory(Comment::class)->create([

2. 'post_id' => 2,

3. 'user_id' => 3,

4. ]);

On utilise une méthode du package baum (makeChildOf) pour hiérarchiser les commentaires :

1. factory(Comment::class)->create([

2. 'post_id' => 4,

3. 'user_id' => 5,

4. //'parent_id' => $nbrComments,

5. ])->makeChildOf($comment2);

VIII-J - En résumé

On a vu dans ce chapitre comment utiliser un package pour gérer une situation particulière, ici un arbre de données hiérarchiques en simplifiant ainsi le codage final.

IX - La pagination

La pagination consiste à limiter le nombre de données affichées et à prévoir des boutons de navigation entre les pages ainsi constituées. Laravel propose une pagination automatique facile à mettre en œuvre aussi bien au niveau du Query Builder que d'Eloquent. Nous allons voir dans ce chapitre comment faire.

IX-A - Paginer (paginate) le résultat d'une requête

Dans l'application d'exemple en page d'accueil on a 6 articles présentés accompagnés d'une pagination :

On peut ainsi naviguer dans les pages avec facilité.

Si on regarde le code dans le contrôleur Front/PostController :

1. public function index()

2. {

3. $posts = $this->postRepository->getActiveOrderByDate($this->nbrPages);

5. return view('front.index', compact('posts'));

6. }

On voit qu'on fait appel à la méthode getActiveOrderByDate du repository PostRepository :

1. protected function queryActiveOrderByDate()

2. {

3. return $this->model

4. ->select('id', 'title', 'slug', 'excerpt', 'image')

5. ->whereActive(true)

6. ->latest();

7. }

9. ...

11. public function getActiveOrderByDate($nbrPages)

12. {

13. return $this->queryActiveOrderByDate()->paginate($nbrPages);

14. }

Dans la méthode queryActiveOrderByDate on :

- sélectionne (select) les colonnes nécessaires ;

- filtre (whereActive) uniquement les articles actifs (true) ;

- ordonne à partir du plus récent (latest).

Dans la méthode getActiveOrderByDate on reçoit en paramètre le nombre d'articles à afficher pour chaque page et on utilise la méthode paginate sur la requête pour avoir automatiquement le bon nombre d'enregistrements.

Au niveau de la vue front/index.blade.php on a ce code :

{{ $posts->links('front.pagination') }}

La méthode links génère la pagination. Par défaut elle ne nécessite pas de paramètre, on verra plus loin pourquoi ici on lui transmet une vue.

Si vous avez la curiosité de regarder comment sont constitués les liens de la pagination vous allez voir quelque chose de cette forme :

http://monsite.com/?page=2

Donc on a en paramètre de l'URL le numéro de la page désirée.

IX-B - Ajouter (appends) des paramètres à la pagination

Il arrive qu'on ait besoin d'ajouter des paramètres à l'URL de la pagination. Regardez dans l'administration la liste des contacts en sélectionnant les nouveaux :

Pour la sélection des nouveaux on utilise un paramètre dans l'URL :

http://monsite.com/admin/contacts?new=on

Du coup au niveau de la pagination il ne faut pas perdre ce paramètre :

http://monsite.com/admin/contacts?new=on&page=2

Voyons comment on fait ça…

On a vu que pour la gestion des pages d'index des différentes entités il est fait usage du trait Back/Indexable. Dans ce trait on a la méthode qui gère la liste des entités :

1. public function index(Request $request)

2. {

3. $parameters = $this->getParameters ($request);

5. // Get records and generate links for pagination

6. $records = $this->repository->getAll (config ("app.nbrPages.back.$this->table"), $parameters);

7. $links = $records->appends ($parameters)->links ('back.pagination');

9. // Ajax response

10. if ($request->ajax ()) {

11. return response ()->json ([

12. 'table' => view ("back.$this->table.table", [$this->table => $records])->render (),

13. 'pagination' => $links->toHtml (),

14. ]);

15. }

17. return view ("back.$this->table.index", [$this->table => $records, 'links' => $links]);

18. }

Comme ça concerne plusieurs entités aux paramètres variables on a une méthode getParameters qui va chercher ces paramètres. Ce qui nous intéresse pour ce chapitre c'est cette partie du code :

$records = $this->repository->getAll (config ("app.nbrPages.back.$this->table"), $parameters);
$links = $records->appends ($parameters)->links ('back.pagination');

On va chercher les enregistrements (getAll) dans le repository en transmettant le nombre de pages issu de la configuration (config("app.nbrPages.back.$this->table")) ainsi que les paramètres.

Comme on a affaire aux contacts c'est dans ce cas le repository ContactRepository :

1. public function getAll($nbrPages, $parameters)

2. {

3. return Contact::with ('ingoing')

4. ->latest()

5. ->when ($parameters['new'], function ($query) {

6. $query->has ('ingoing');

7. })->paginate($nbrPages);

8. }

On voit qu'on :

- charge une relation (with('ingoing')) dont j'ai déjà parlé dans le chapitre sur le polymorphisme ;

- quand (when) on a le paramètre new on filtre (has) les nouveaux contacts ;

- on active la pagination (paginate).

Si on en revient au code du trait :

$links = $records->appends ($parameters)->links ('back.pagination');

On voit qu'on utilise la méthode appends pour ajouter le paramètre new, ensuite on utilise la méthode links qu'on a déjà vue ci-dessus.

Du coup dans la vue back/contacts/index.blade.php on a juste ce code pour générer la pagination :

{{ $links }}

IX-C - La pagination en Ajax

Toujours avec la vue d'administration des contacts vous avez sans doute remarqué que le changement de page se fait en Ajax, c'est juste la liste des contacts qui est regénérée.

Toujours dans la méthode index du trait qu'on a vu ci-dessus on a ce code :

1. if ($request->ajax ()) {

2. return response ()->json ([

3. 'table' => view ("back.$this->table.table", [$this->table => $records])->render (),

4. 'pagination' => $links->toHtml (),

5. ]);

6. }

Si la requête est en Ajax (ajax) alors on retourne une réponse JSON (response()->json) avec deux éléments :

- la liste (table) issue de la vue dont on génère le code HTML (render) ;

- la pagination (pagination) avec les liens dont on a vu la génération ci-dessus et qu'on envoie au format HTML (toHtml).

Il suffit côté client de regénérer les parties de la page correspondante :

$('#pannel').html(data.table)
$('#pagination').html(data.pagination)

Je parlerai plus en détail de l'utilisation d'Ajax avec Laravel dans un chapitre ultérieur.

IX-D - La personnalisation de la vue de la pagination

Par défaut la vue de génération des liens de la pagination correspond à une mise en forme pour Bootstrap. Quand on l'utilise c'est parfait et on n'a rien à faire. Par contre quand on désire une autre mise en forme, comme c'est le cas pour l'application d'exemple on a un peu plus de travail.

Une solution consiste à publier les vues de Laravel avec la commande :

php artisan vendor:publish --tag=laravel-pagination

On se retrouve avec les vues dans le dossier resources/views/vendor/pagination et il ne reste plus qu'à les modifier.

Une autre solution que j'ai adoptée pour l'application est de passer le nom de la vue à la méthode links. Par exemple pour la page d'accueil on a ce code dans la vue :

{{ $posts->links('front.pagination') }}

Ce qui signifie qu'on utilise la vue front/pagination pour la mise en forme des liens de pagination :

1. @if ($paginator->hasPages())

2. <nav class="pagination">

3. {{-- Previous Page Link --}}

4. @if ($paginator->onFirstPage())

5. <span class="page-numbers prev inactive">@lang('pagination.previous')</span>

6. @else

7. <a href="{{ $paginator->previousPageUrl() }}" class="page-numbers prev" rel="prev">@lang('pagination.previous')</a>

8. @endif

10. {{-- Pagination Elements --}}

11. @foreach ($elements as $element)

12. {{-- "Three Dots" Separator --}}

13. @if (is_string($element))

14. <span class="page-numbers current">{{ $element }}</span>

15. @endif

17. {{-- Array Of Links --}}

18. @if (is_array($element))

19. @foreach ($element as $page => $url)

20. @if ($page == $paginator->currentPage())

21. <span class="page-numbers current">{{ $page }}</span>

22. @else

23. <a href="{{ $url }}" class="page-numbers">{{ $page }}</a>

24. @endif

25. @endforeach

26. @endif

27. @endforeach

29. {{-- Next Page Link --}}

30. @if ($paginator->hasMorePages())

31. <a href="{{ $paginator->nextPageUrl() }}"  class="page-numbers next" rel="next">@lang('pagination.next')</a>

32. @else

33. <span class="page-numbers next inactive">@lang('pagination.next')</span>

34. @endif

35. </nav>

36. @endif

Pour la partie administration on a une autre mise en forme et donc une autre vue back/pagination :

1. @if ($paginator->hasPages())

2. <ul class="pagination pagination-sm no-margin pull-right">

3. {{-- Previous Page Link --}}

4. @if ($paginator->onFirstPage())

5. <li class="disabled"><a href="#">&laquo;</a></li>

6. @else

7. <li><a href="{{ $paginator->previousPageUrl() }}" rel="prev">&laquo;</a></li>

8. @endif

10. {{-- Pagination Elements --}}

11. @foreach ($elements as $element)

12. {{-- "Three Dots" Separator --}}

13. @if (is_string($element))

14. <li class="disabled"><a href="#">{{ $element }}</a></li>

15. @endif

17. {{-- Array Of Links --}}

18. @if (is_array($element))

19. @foreach ($element as $page => $url)

20. @if ($page == $paginator->currentPage())

21. <li class="active"><a href="#">{{ $page }}</a></li>

22. @else

23. <li><a href="{{ $url }}">{{ $page }}</a></li>

24. @endif

25. @endforeach

26. @endif

27. @endforeach

29. {{-- Next Page Link --}}

30. @if ($paginator->hasMorePages())

31. <li><a href="{{ $paginator->nextPageUrl() }}" rel="next">&raquo;</a></li>

32. @else

33. <li class="disabled"><a href="#">&raquo;</a></li>

34. @endif

35. </ul>

36. @endif

On a toujours la même structure mais ce sont les classes utilisées qui changent selon l'aspect voulu.

IX-E - En résumé

- La pagination avec Laravel se fait simplement avec la méthode paginate.

- On peut ajouter des paramètres aux URL de pagination avec la méthode appends.

- On peut facilement réaliser une pagination en Ajax.

- Il faut créer une vue particulière pour la pagination si on n'utilise pas Bootstrap.

X - Les ressources

Avec Laravel on peut créer facilement un routage et un contrôleur REST/CRUD, autrement appelé contrôleur de ressources. Il est aussi aisé de bâtir des API et de travailler avec du JSON. Dans ce chapitre je vous propose d'explorer cet aspect de Laravel.

X-A - Préparation

Pour ce chapitre on va partir d'une installation vierge de Laravel :

composer create-project --prefer-dist laravel/laravel laravel5

Complétée par l'authentification :

php artisan make:auth

Et par une migration (avec la bonne configuration dans le fichier .env) :

php artisan migrate

Vous devez ainsi avoir un Laravel fonctionnel :

Et les tables dans la base :

On va utiliser tinker pour créer quelques utilisateurs :

1. php artisan tinker

3. Psy Shell v0.8.11 (PHP 7.1.9 — cli) by Justin Hileman

5. >>> factory(App\User::class, 10)->create()

7. => Illuminate\Database\Eloquent\Collection {#765

8. all: [

9. App\User {#761

10. name: "Rigoberto Balistreri Sr.",

11. email: "xstroman@example.com",

12. updated_at: "2017-09-30 22:03:29",

13. created_at: "2017-09-30 22:03:29",

14. id: 1,

15. },

16. ...

J'ai déjà parlé des factories dans un précédent chapitre.

On utilise alors la méthode create d'Eloquent pour enregistrer les données dans la base :

Vous voyez ce qu'on peut faire avec juste une ligne dans tinker !

X-B - Les contrôleurs de ressource

X-B-1 - Le contrôleur

Lorsque vous créez un contrôleur vous devez déterminer le nom des méthodes, les créer puis les coder. Un contrôleur de ressource peut bien vous aider dans cette tâche en normalisant (REST/CRUD) les méthodes. Pour créer un contrôleur de ressource c'est tout simple :

php artisan make:controller UserController --resource

Vous avez alors la création du contrôleur comme on l'a déjà vue dans ce cours :

Mais le fait d'ajouter l'option --resource crée ce code :

1. <?php

3. namespace App\Http\Controllers;

5. use Illuminate\Http\Request;

7. class UserController extends Controller

8. {

9. /**

10. * Display a listing of the resource.

11. *

12. * @return \Illuminate\Http\Response

13. */

14. public function index()

15. {

16. //

17. }

19. /**

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

21. *

22. * @return \Illuminate\Http\Response

23. */

24. public function create()

25. {

26. //

27. }

29. /**

30. * Store a newly created resource in storage.

31. *

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

33. * @return \Illuminate\Http\Response

34. */

35. public function store(Request $request)

36. {

37. //

38. }

40. /**

41. * Display the specified resource.

42. *

43. * @param  int  $id

44. * @return \Illuminate\Http\Response

45. */

46. public function show($id)

47. {

48. //

49. }

51. /**

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

53. *

54. * @param  int  $id

55. * @return \Illuminate\Http\Response

56. */

57. public function edit($id)

58. {

59. //

60. }

62. /**

63. * Update the specified resource in storage.

64. *

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

66. * @param  int  $id

67. * @return \Illuminate\Http\Response

68. */

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

70. {

71. //

72. }

74. /**

75. * Remove the specified resource from storage.

76. *

77. * @param  int  $id

78. * @return \Illuminate\Http\Response

79. */

80. public function destroy($id)

81. {

82. //

83. }

84. }

On se retrouve avec sept méthodes correspondant aux tâches classiques :

- index : afficher la liste des utilisateurs ;

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

- store : créer un utilisateur ;

- show : afficher l'utilisateur ;

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

- update : modifier un utilisateur ;

- destroy : supprimer un utilisateur.

Évidemment il vous reste à coder toutes ces méthodes…

X-B-2 - Les routes

Pour générer les routes vers les sept méthodes du contrôleur de ressource c'est aussi très simple. Entrez ce code dans le fichier routes/web.php :

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

Si maintenant vous regardez les routes générées avec :

php artisan route:list

Vous obtenez ceci :

On a sept routes, chacune correspondant à une méthode du contrôleur :

Verbe

URL

Méthode du contrôleur

Nom

Description

GET

users

index()

users.index

Liste de tous les utilisateurs

GET

users/create

create()

users.create

Affichage formulaire de création

POST

users

store()

users.store

Création de l'utilisateur

GET

users/{user}

show()

users.show

Affichage de l'utilisateur

GET

users/{user}/edit

edit()

users.edit

Affichage formulaire de modification

PUT/PATCH

users/{user}

update()

users.update

Modification de l'utilisateur

DELETE

users/{user}

destroy()

users.destroy

Suppression de l'utilisateur

X-B-3 - Un peu de code

X-B-3-a - Liste des utilisateurs

Maintenant qu'on a un contrôleur et des routes dédiées il suffit de compléter le code. On va commencer par déclarer l'espace de nom pour le modèle pour simplifier la syntaxe :

use App\User;

Ensuite on code la méthode index :

1. public function index()

2. {

3. $users = User::all();

4. foreach ($users as $user) {

5. echo $user->name . '<br>';

6. }

7. }

Et pour l'URL …/users on obtient quelque chose dans ce genre :

1. Rigoberto Balistreri Sr.

2. Noble Fisher Sr.

3. Brenna Mohr

4. Helen Graham

5. Gardner Trantow

6. Adrianna Kautzer

7. Eladio Douglas IV

8. Susie Koch

9. Mr. Ned Harvey V

10. Kareem Hirthe

X-B-3-b - Afficher un utilisateur

C'est la méthode show qui est concernée :

1. public function show(User $user)

2. {

3. echo 'Nom :' . $user->name . '<br>';

4. echo 'Email :' . $user->email . '<br>';

5. }

On a déjà vu dans un précédent chapitre la liaison implicite entre le paramètre dans l'URL et un modèle.

Avec une URL du genre …users/1 on obtient :

Nom :Rigoberto Balistreri Sr.
Email :xstroman@example.com

X-B-3-c - Créer un utilisateur

Pour la création d'un utilisateur deux méthodes sont concernées :

1. public function create()

2. {

3. return view('create');

4. }

6. public function store(Request $request)

7. {

8. User::create($request->all());

10. return "Utilisateur créé !";

11. }

Pour simplifier je n'ai pas prévu de validation.

Voici la vue create :

1. @extends('layouts.app')

3. @section('content')

4. <div class="container">

5. <div class="row">

6. <div class="col-md-8 col-md-offset-2">

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

8. <div class="panel-heading">Créer un utilisateur</div>

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

11. <form class="form-horizontal" method="POST" action="{{ route('users.store') }}">

12. {{ csrf_field() }}

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

15. <label for="name" class="col-md-4 control-label">Nom</label>

17. <div class="col-md-6">

18. <input id="name" type="text" class="form-control" name="name" required autofocus>

19. </div>

20. </div>

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

23. <label for="email" class="col-md-4 control-label">E-Mail</label>

25. <div class="col-md-6">

26. <input id="email" type="email" class="form-control" name="email" required>

27. </div>

28. </div>

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

31. <label for="password" class="col-md-4 control-label">Mot de passe</label>

33. <div class="col-md-6">

34. <input id="password" type="password" class="form-control" name="password" required>

35. </div>

36. </div>

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

39. <div class="col-md-8 col-md-offset-4">

40. <button type="submit" class="btn btn-primary">

41. Enregistrer

42. </button>

43. </div>

44. </div>

45. </form>

46. </div>

47. </div>

48. </div>

49. </div>

50. </div>

51. @endsection

Avec cet aspect (en utilisant le template par défaut de Laravel) en utilisant l'URL …/users/create :

Remarquez dans le formulaire l'utilisation du verbe POST et la génération de l'action à partir du nom de la route :

<form class="form-horizontal" method="POST" action="{{ route('users.store') }}">

X-B-3-d - Modifier un utilisateur

Pour la modification d'un utilisateur ce sont ces deux méthodes :

1. public function edit(User $user)

2. {

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

4. }

7. public function update(Request $request, User $user)

8. {

9. $user->update($request->all());

11. return "Utilisateur modifié !";

12. }

Pour simplifier je n'ai pas prévu de validation.

Voici la vue edit :

1. @extends('layouts.app')

3. @section('content')

4. <div class="container">

5. <div class="row">

6. <div class="col-md-8 col-md-offset-2">

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

8. <div class="panel-heading">Modifier un utilisateur</div>

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

11. <form class="form-horizontal" method="POST" action="{{ route('users.update', $user->id) }}">

12. {{ csrf_field() }}

13. {{ method_field('PUT') }}

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

16. <label for="name" class="col-md-4 control-label">Nom</label>

18. <div class="col-md-6">

19. <input id="name" type="text" class="form-control" name="name" value="{{ $user->name }}" required autofocus>

20. </div>

21. </div>

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

24. <label for="email" class="col-md-4 control-label">E-Mail</label>

26. <div class="col-md-6">

27. <input id="email" type="email" class="form-control" name="email" value="{{ $user->email }}" required>

28. </div>

29. </div>

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

32. <div class="col-md-8 col-md-offset-4">

33. <button type="submit" class="btn btn-primary">

34. Enregistrer

35. </button>

36. </div>

37. </div>

38. </form>

39. </div>

40. </div>

41. </div>

42. </div>

43. </div>

44. @endsection

On a du code commun avec la vue pour la création et dans un projet réel il serait judicieux de mutualiser ce code dans un template.

Avec cet aspect (en utilisant le template par défaut de Laravel) en utilisant l'URL …/users/1/edit :

Remarquez dans le formulaire la génération de l'action à partir du nom de la route (avec transmission de l'identifiant de l'utilisateur en paramètre) :

<form class="form-horizontal" method="POST" action="{{ route('users.update', $user->id) }}">

D'autre part, comme on l'a déjà vu dans un précédent chapitre, le navigateur ne sait pas gérer le verbe PUT, donc on lui indique un verbe POST mais on ajoute un champ caché en précisant le bon verbe :

{{ method_field('PUT') }}

X-B-3-e - Supprimer un utilisateur

C'est la méthode destroy qui est concernée :

1. public function destroy(User $user)

2. {

3. $user->delete();

5. return 'Utilisateur supprimé !';

6. }

Avec une URL du genre …users/1 et le verbe DELETE on supprime l'utilisateur correspondant.

Pour voir ça en action on va ajouter une route :

Route::get('users/{user}/destroy', 'UserController@destroyForm');

Une méthode dans le contrôleur :

1. public function destroyForm(User $user)

2. {

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

4. }

Et cette vue destroy :

1. @extends('layouts.app')

3. @section('content')

4. <div class="container">

5. <div class="row">

6. <div class="col-md-8 col-md-offset-2">

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

8. <div class="panel-heading">Supprimer un utilisateur</div>

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

11. <form class="form-horizontal" method="POST" action="{{ route('users.destroy', $user->id) }}">

12. {{ csrf_field() }}

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

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

15. <div class="col-md-8 col-md-offset-4">

16. <button type="submit" class="btn btn-primary">

17. Supprimer l'utilisateur {{ $user->name }}

18. </button>

19. </div>

20. </div>

21. </form>

22. </div>

23. </div>

24. </div>

25. </div>

26. </div>

27. @endsection

Avec cet aspect pour une URL dans le genre …users/1/destroy :

X-C - L'application d'exemple

Dans l'application d'exemple on utilise évidemment des ressources comme on l'a vu ci-dessus mais pas toujours de façon complète. Par exemple pour les contacts on utilise seulement (only) les méthodes create et store pour le front :

Route::resource('contacts', 'Front\ContactController', ['only' => ['create', 'store']]);

Et les méthodes index et destroy pour le back :

Route::resource('contacts', 'ContactController', ['only' => ['index', 'destroy']]);

Pour les commentaires on utilise seulement (only) les méthodes update et destroy pour le front :

Route::resource('comments', 'Front\CommentController', ['only' => ['update', 'destroy']]);

Et les méthodes index et destroy pour le back :

Route::resource('comments', 'CommentController', ['only' => ['index', 'destroy']]);

Pour les utilisateurs dans le back on utilise seulement (only) les méthodes index, edit, update et destroy :

Route::resource('users', 'UserController', ['only' => ['index', 'edit', 'update', 'destroy']]);

On pourrait adopter la logique inverse et utiliser except au lieu de only.

Par contre pour les articles en back la ressource est complète :

Route::resource('posts', 'PostController');

Classiquement avec la méthode index on affiche la liste paginée de la ressource avec des boutons permettant les opérations de modification ou suppression. Par exemple dans l'administration pour les utilisateurs on a :

Pour chaque utilisateur on affiche les données et on a deux boutons à droite pour la modification et la suppression. Le premier est un simple lien du genre …admin/users/1/edit alors que le second est traité en Ajax.

Pour les articles on affiche 3 boutons :

On a en plus le bouton (vert) pour visualiser l'article.

X-D - Les API REST

REST (Representational State Transfer) est un style d'architecture pour constituer des API. Pour Laravel une API REST :

- est organisée autour de ressources ;

- est sans persistance (aucune session) ;

- est orientée client-serveur ;

- peut être mise en cache (donc une requête doit retourner toujours les mêmes résultats) ;

- doit retourner du JSON…

Si vous n'êtes pas trop sûr de vos connaissances concernant les API REST, je vous conseille ce cours.

Pour continuer avec notre ressource d'utilisateurs on peut imaginer ce endpoint pour les informations de tous les utilisateurs :

1. GET /api/users

2. [

3. {

4. id: 1,

5. name: 'Rigoberto Balistreri Sr. Noble Fisher Sr.'

6. },

7. {

8. id: 2,

9. name: 'Noble Fisher Sr.'

10. }

11. ...

12. ]

Ou ce endpoint pour les informations concernant un utilisateur :

1. GET /api/users/2

2. {

3. id: 2,

4. name: 'Noble Fisher Sr.'

5. }

On peut imaginer d'autres endpoints pour modifier, ajouter, supprimer…

Il ne reste plus qu'à voir comment on réalise ça avec Laravel mais vous en avez désormais une idée déjà assez précise avec ce qu'on a déjà vu dans ce chapitre.

X-D-1 - Le contrôleur

On va créer le contrôleur :

php artisan make:controller Api/UserController --resource

On le retrouve rangé dans le dossier app/Http/Controllers/Api créé par la même occasion :

Avec évidemment le même code que celui qu'on a eu pour la même commande ci-dessus (et Laravel se débrouille tout seul pour les espaces de nom) :

1. <?php

3. namespace App\Http\Controllers\Api;

5. use Illuminate\Http\Request;

6. use App\Http\Controllers\Controller;

8. class UserController extends Controller

9. {

10. /**

11. * Display a listing of the resource.

12. *

13. * @return \Illuminate\Http\Response

14. */

15. public function index()

16. {

17. //

18. }

20. /**

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

22. *

23. * @return \Illuminate\Http\Response

24. */

25. public function create()

26. {

27. //

28. }

30. /**

31. * Store a newly created resource in storage.

32. *

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

34. * @return \Illuminate\Http\Response

35. */

36. public function store(Request $request)

37. {

38. //

39. }

41. /**

42. * Display the specified resource.

43. *

44. * @param  int  $id

45. * @return \Illuminate\Http\Response

46. */

47. public function show($id)

48. {

49. //

50. }

52. /**

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

54. *

55. * @param  int  $id

56. * @return \Illuminate\Http\Response

57. */

58. public function edit($id)

59. {

60. //

61. }

63. /**

64. * Update the specified resource in storage.

65. *

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

67. * @param  int  $id

68. * @return \Illuminate\Http\Response

69. */

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

71. {

72. //

73. }

75. /**

76. * Remove the specified resource from storage.

77. *

78. * @param  int  $id

79. * @return \Illuminate\Http\Response

80. */

81. public function destroy($id)

82. {

83. //

84. }

85. }

On va supprimer les méthodes create et edit qui n'ont pas lieu d'être pour une API.

Et on va coder les autres méthodes :

1. <?php

3. namespace App\Http\Controllers\Api;

5. use Illuminate\Http\Request;

6. use App\Http\Controllers\Controller;

7. use App\User;

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. return User::all();

19. }

21. /**

22. * Store a newly created resource in storage.

23. *

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

25. * @return \Illuminate\Http\Response

26. */

27. public function store(Request $request)

28. {

29. User::create($request->all());

30. }

32. /**

33. * Display the specified resource.

34. *

35. * @param  int  $id

36. * @return \Illuminate\Http\Response

37. */

38. public function show(User $user)

39. {

40. return $user;

41. }

43. /**

44. * Update the specified resource in storage.

45. *

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

47. * @param  int  $id

48. * @return \Illuminate\Http\Response

49. */

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

51. {

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

53. }

55. /**

56. * Remove the specified resource from storage.

57. *

58. * @param  int  $id

59. * @return \Illuminate\Http\Response

60. */

61. public function destroy(User $user)

62. {

63. $user->delete();

64. }

65. }

Évidemment ça ressemble énormément à ce qu'on a fait précédemment et c'est normal puisqu'il s'agit du même genre de traitement sur les mêmes données.

X-D-2 - Les routes

Pour les routes on adopte aussi une ressource dans le fichier routes/api.php :

Route::namespace('Api')->group(function() {
Route::resource('users', 'UserController', ['except' => ['create', 'edit']]);
});

Ce qui donne ces routes :

Le fait de déclarer ces routes dans le fichier routes/api.php a pour effet de leur appliquer le middleware api au lieu de web. Si vous regardez dans le fichier app/Http/Kernel.php vous allez voir la différence que ça génère :

1. protected $middlewareGroups = [

2. 'web' => [

3. \App\Http\Middleware\EncryptCookies::class,

4. \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,

5. \Illuminate\Session\Middleware\StartSession::class,

6. // \Illuminate\Session\Middleware\AuthenticateSession::class,

7. \Illuminate\View\Middleware\ShareErrorsFromSession::class,

8. \App\Http\Middleware\VerifyCsrfToken::class,

9. \Illuminate\Routing\Middleware\SubstituteBindings::class,

10. ],

12. 'api' => [

13. 'throttle:60,1',

14. 'bindings',

15. ],

16. ];

Comme on est sans état : plus de cookie, de session, de protection CSRF…

X-D-3 - Le fonctionnement

Maintenant avec l'URL …api/users on obtient une réponse JSON (Laravel convertit automatiquement la réponse en JSON sans qu'on lui demande) :

On obtient toutes les colonnes non cachées, c'est-à-dire celles qui ne sont pas définies dans la propriété $hidden du modèle :

protected $hidden = [
'password', 'remember_token',
];

Avec l'URL …/api/users/2 on obtient :

On pourrait aussi imaginer utiliser une pagination pour cette API. Il suffit de changer la méthode index :

1. public function index()

2. {

3. return User::paginate(4);

4. }

On peut alors demander la page qu'on veut, par exemple la seconde avec l'URL …api/users?page=2 :

On ne reçoit que les informations de la page 2 et en plus des renseignements concernant la pagination.

On peut imaginer aussi filtrer, ordonner…

Je ne traite pas dans ce chapitre de l'authentification spécifique aux API. Je vous invite à regarder la documentation de Passport sur le sujet.

X-E - Les ressources d'API

On a vu ci-dessus comme il est facile de retourner une réponse JSON à partir des données d'un modèle. C'est parfait tant qu'on veut une simple sérialisation des valeurs des colonnes. Mais parfois on veut effectuer quelques traitements intermédiaires (ça correspond au design pattern transformer). Imaginons que nous voulions retourner seulement les deux premières lettres des noms, c'est une idée bizarre mais pourquoi pas ?

On utilise Artisan pour créer la ressource :

php artisan make:resource User

On se retrouve avec la ressource ici :

Avec ce code :

1. <?php

3. namespace App\Http\Resources;

5. use Illuminate\Http\Resources\Json\Resource;

7. class User extends Resource

8. {

9. /**

10. * Transform the resource into an array.

11. *

12. * @param  \Illuminate\Http\Request

13. * @return array

14. */

15. public function toArray($request)

16. {

17. return parent::toArray($request);

18. }

19. }

Là on se contente de tout sérialiser, ça ne va donc pas changer grand-chose à ce qu'on avait précédemment. Dans le contrôleur on utilise la ressource, par exemple pour la méthode show :

1. use App\Http\Resources\User as UserResource;

3. ...

5. public function show(User $user)

6. {

7. return new UserResource($user);

8. }

Maintenant pour l'URL …/api/users/2 on obtient :

Maintenant changeons la ressource :

1. public function toArray($request)

2. {

3. return [

4. 'id' => $this->id,

5. 'name' => substr($this->name, 0, 2),

6. 'email' => $this->email,

7. 'created_at' => $this->created_at,

8. 'updated_at' => $this->updated_at,

9. ];

10. }

Maintenant pour l'URL …/api/users/2 on obtient :

On voit qu'on n'a plus que les deux premiers caractères du nom. On a bien effectué un traitement intermédiaire sur les valeurs retournées.

Il y aurait encore beaucoup à dire sur les ressources et les API mais vous avez maintenant une bonne base de départ.

X-F - En résumé

- Un contrôleur de ressource permet de normaliser les opérations CRUD. Sa création et son routage sont très simples.

- Laravel permet de créer facilement une API et de permettre des transformations avec des ressources.

XI - Les assistants

Laravel est équipé d'Artisan qui simplifie bien des tâches mais malgré ses possibilités étendues il ne sait quand même pas tout faire. Un certain nombre d’outils plus ou moins spécialisés ont été créés pour faire gagner du temps dans le développement d'une application. Je vous propose dans ce chapitre un petit tour d'horizon de ce qui existe actuellement en faisant une sélection assez étroite. Pour avoir une vue complète de ce qui est disponible allez voir cette page.

XI-A - Laravel Schema Designer

Laravel Schema Designer est sans doute l'outil que j'utilise le plus. Il propose une interface graphique conviviale pour construire le schéma d'une base de données avec des formulaires bien conçus :

Il sait très bien gérer les relations :

Et il les représente visuellement avec des traits entre les tables.

Lorsque le schéma est construit on peut ensuite exporter :

- routes ;

- modèles ;

- contrôleurs ;

- migrations ;

- populations (seed) ;

- vues.

Autrement dit on se retrouve avec une trame de base déjà bien élaborée !

Un concurrent est apparu récemment : Laravel Database Designer :

Il est plus esthétique mais ne génère pas grand-chose…

XI-B - InfyOm

InfyOm est un superbe générateur :

Il permet de générer :

- modèle ;

- repository ;

- migrations ;

- requêtes ;

- API ;

- CRUD vues avec la validation.

Je lui ai consacré un article complet sur ce blog et je ne vais donc pas m'étendre ici.

XI-C - MySQL Workbench Export

MySQL Workbench est un superbe outil pour gérer une base MySQL :

Avec le « reverse engineer » on peut récupérer le schéma d'un base existante :

Mais ce qui est intéressant c'est qu'il existe un plugin pour faire l'inverse !

Un fois qu'on l'a installé on peut entre autres exporter les migrations :

Sans passer par le Workbench il existe aussi un package qui permet de générer des migrations à partir d'une base existante.

XI-D - Il n'y a pas qu'Eloquent

Même si Eloquent est performant on a d'autres choix, en particulier Doctrine et MongoDB.

XI-D-1 - Laravel Doctrine

Le package Laravel Doctrine permet d'utiliser Doctrine avec Laravel :

Eloquent respecte le pattern « Active Record » (en gros un enregistrement dans une table est traité comme un objet par Eloquent). Doctrine est lui fondé sur le pattern « Data Mapper » (il y a une couche intermédiaire entre la base et les objets). Sans entrer dans le débat entre les amateurs de l'un et de l'autre on va dire qu'Eloquent est fortement couplé à la base alors que Doctrine sépare bien les aspects persistance et entités.

Donc si vous voulez essayer Doctrine ou si vous êtes déjà convaincu (par exemple parce que vous venez du monde Symfony) alors utilisez ce package.

XI-D-2 - Laravel MongoDB

Le package Laravel MongoDB permet d'utiliser MongoDB avec Laravel :

MongoDB permet une approche NoSQL. C'est assez dépaysant quand on a l'habitude d'utiliser SQL. Les informations ne sont plus sagement rangées dans des lignes d'une table mais dans des documents JSON. Il y a pas mal de tutoriels d'introduction, par exemple celui-ci.

XI-E - En résumé

- Il existe de nombreux outils et packages pour faciliter l'écriture du code avec Laravel.

- Laravel Schema Designer permet de générer du code à partir d'une construction visuelle d'une base de données.

- InfyOm est un générateur très complet fonctionnant principalement en ligne de commande.

- Un plugin de MySQL Workbench permet de générer des migrations.

- Deux packages permettent d'utiliser Doctrine ou MongoDB avec Laravel.