Cours pour apprendre à utiliser le framework Laravel 5.5

Plus loin avec Laravel

Maurice Chavelli

Il y a plusieurs façons de programmer. En général la plus simple et la plus lisible consiste à procéder de façon séquentielle : on a une suite d'instructions (modulables par des conditions) qui sont toujours les mêmes. Le flux est facile à suivre.

Mais on a aussi l'approche événementielle. On va réagir à des événements susceptibles de se produire. On peut mettre ainsi en place un ensemble constitué par une partie qui détecte des événements et une autre qui les gère.

« Laravel » permet de gérer avec facilité les événements comme on va le voir dans ce chapitre.

Commentez

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

- Auteur : Maurice Chavelli

- Parution : 22 octobre 2018

- Licence : Copyright ® 2018 Maurice Chavelli. 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. Les événements

- I-A. Le design pattern Observateur

- I-B. Organisation du code

- I-B-1. Les événements déjà présents dans Laravel

- I-B-2. Une application de test

- I-B-3. Le fournisseur de service (service provider)

- I-C. On crée une écoute

- I-D. On crée un événement

- I-E. L'application d'exemple

- I-E-1. Modèle créé

- I-E-2. Article modifié

- I-F. En résumé

- II. La localisation

- II-A. Le principe

- II-A-1. Les fichiers de langage

- II-A-1-a. Clés et valeurs

- II-A-1-b. Les textes

- II-A-2. Configuration de la locale

- II-A-3. Dans le code

- II-A-3-a. L'helper __

- II-A-3-b. Blade

- II-B. Le pluriel

- II-C. Des assistants

- II-D. Les noms pour la validation

- II-E. Les dates

- II-F. En résumé

- III. Les notifications

- III-A. Organisation du code

- III-B. Le renouvellement du mot de passe

- III-C. La notification des rédacteurs pour les nouveaux commentaires

- III-C-1. La table des notifications

- III-C-2. La notification

- III-C-3. Création d'une notification

- III-C-4. Les routes et le contrôleur

- III-C-5. La vue index

- III-D. En résumé

- IV. La sécurité

- IV-A. L'authentification

- IV-A-1. Les middlewares

- IV-A-2. Le throttle

- IV-A-3. Le hachage

- IV-B. Quelques éléments à prendre en considération

- IV-B-1. Les injections SQL

- IV-B-2. CSRF

- IV-B-3. XSS (Cross-Site Scripting)

- IV-B-4. L'assignation de masse

- IV-B-5. Les requêtes de formulaire

- IV-C. Les autorisations

- IV-D. En résumé

- V. Ajax

- V-A. Les messages dans l'application

- V-B. La vue des messages

- V-C. Le JavaScript

- V-D. La protection CSRF

- V-E. Le traitement côté serveur

- V-F. Les commentaires

- V-G. En résumé

- VI. Les vues

- VI-A. L'héritage

- VI-B. L'inclusion

- VI-B-1. Les panneaux du tableau de bord

- VI-B-2. Le menu latéral de l'administration

- VI-C. Les composants

- VI-D. Les structures de contrôle

- VI-D-1. @if

- VI-D-2. @else

- VI-D-3. @unless

- VI-D-4. @isset et @empty

- VI-D-5. @auth et @guest

- VI-D-6. Condition personnalisée

- VI-D-7. Les boucles

- VI-E. L'injection de service

- VI-F. Les composeurs de vue (view composers)

- VI-G. En résumé

- VII. Les tests

- VII-A. L'intendance des tests

- VII-A-1. PHPUnit

- VII-B. L'intendance de Laravel

- VII-B-1. L'environnement de test

- VII-C. Construire un test

- VII-C-1. Les trois étapes d'un test

- VII-D. Assertions et appel de route

- VII-D-1. Les assertions

- VII-D-2. Appel de route et test de réponse

- VII-E. Les vues et les contrôleurs

- VII-E-1. Les vues

- VII-E-2. Les contrôleurs

- VII-F. Isoler les tests

- VII-F-1. Simuler une classe

- VII-G. Tester une application (dusk)

- VII-G-1. Installation

- VII-G-2. Exemples

- VII-H. En résumé

- VIII. Les fichiers et le cache

- VIII-A. Le système de gestion des fichiers

- VIII-A-1. La configuration

- VIII-A-2. La façade

- VIII-B. Un exemple

- VIII-B-1. Les médias

- VIII-B-2. Les thumbs

- VIII-C. Le cache

- VIII-C-1. La configuration

- VIII-C-2. Utilisation

- VIII-C-2-a. Mettre en cache

- VIII-C-2-b. Récupérer une valeur en cache

- VIII-C-2-c. Supprimer une valeur du cache

- VIII-C-2-d. Vérifier qu'une clé existe

- VIII-C-3. Un exemple

- VIII-C-4. Les routes et la configuration

- VIII-C-4-a. Les routes

- VIII-C-4-b. La configuration

- VIII-D. En résumé

- IX. CSS et JavaScript

- IX-A. Le CSS avec Laravel Mix

- IX-A-1. Installation

- IX-A-2. Utilisation

- IX-B. Le JavaScript avec Laravel Mix

- IX-C. Les actions de Laravel Mix

- IX-D. En résumé

- X. Vue.js

- X-A. Le serveur

- X-A-1. Données

- X-A-2. Contrôleur

- X-A-3. Protections

- X-A-4. Routes

- X-B. Le client

- X-B-1. Vue-resource

- X-B-2. La liste des pensées

- X-B-2-a. Une liste simple

- X-B-2-b. On améliore l'aspect

- X-B-2-c. Suppression d'une pensée

- X-B-2-d. Ajout d'une pensée

- X-B-2-e. Les dates en français

- X-B-2-f. La pagination

- X-C. En résumé

I - Les événements

I-A - Le design pattern Observateur

Le design pattern « Observateur » (observer) établit une relation de type 1:n entre des objets. Si l'objet côté 1 change d'état, il en informe les objets côté n pour qu'ils puissent agir en conséquence. Ça implique une intendance et les objets doivent pouvoir s'inscrire et se mettre à l'écoute des événements du sujet.

Voici une visualisation de ce design pattern :

On a un sujet et des observateurs. Les observateurs doivent s'inscrire (on dit aussi s'abonner) auprès du sujet. Lorsque le sujet change d'état, il notifie tous ses abonnés. Un observateur peut aussi se désinscrire. Il ne recevra alors plus les notifications.

Vous pouvez trouver une description détaillée de ce design pattern ici.

I-B - Organisation du code

Laravel implémente ce design pattern et le rend très simple d'utilisation. On va avoir :

- des classes Event placées dans le dossier app/Events :

- des classes Listener placées dans le dossier app/Listeners :

Ces dossiers n'existent pas dans l'installation de base, ils sont ajoutés dès qu'on crée la première classe avec « Artisan » qui dispose de commandes pour le faire :

I-B-1 - Les événements déjà présents dans Laravel

Mais il n'y a pas que les événements que vous allez créer. Vous pouvez utiliser les événements existant déjà dans le framework qui en propose au niveau de l'authentification :

- registered ;

- attempting ;

- authenticated ;

- login ;

- failed ;

- logout ;

- lockout ;

- passwordReset.

On trouve aussi des événements avec « Eloquent » :

- retrieved ;

- creating ;

- created ;

- updating ;

- updated ;

- saving ;

- saved ;

- deleting ;

- deleted ;

- restoring ;

- restored.

Pour tous ces cas, vous n'avez donc pas à vous inquiéter du déclencheur qui existe déjà.

I-B-2 - Une application de test

Pour nos essais, créez une nouvelle application Laravel 5.5 :

composer create-project --prefer-dist laravel/laravel laravel5

Lancez les migrations :

php artisan migrate

puis ajoutez l'authentification :

php artisan make:auth

Créez un utilisateur à partir du formulaire d'enregistrement :

I-B-3 - Le fournisseur de service (service provider)

Il y a un fournisseur de service dédié aux événements :

Par défaut, on a ce code :

1. <?php

3. namespace App\Providers;

5. use Illuminate\Support\Facades\Event;

6. use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

8. class EventServiceProvider extends ServiceProvider

9. {

10. /**

11. * The event listener mappings for the application.

12. *

13. * @var array

14. */

15. protected $listen = [

16. 'App\Events\Event' => [

17. 'App\Listeners\EventListener',

18. ],

19. ];

21. /**

22. * Register any events for your application.

23. *

24. * @return void

25. */

26. public function boot()

27. {

28. parent::boot();

30. //

31. }

32. }

On a une propriété $listen pour les abonnements. C'est un simple tableau qui prend l'événement (event) comme clé et l'observateur (listener) comme valeur.

Laravel prévoit de placer les observateurs (listeners) dans un dossier app/isteners (qui n'existe pas au départ) en tant que classes, ainsi que les événements (events) dans le dossier Events (qui n'existe pas au départ).

I-C - On crée une écoute

On va utiliser l'événement intégré à Laravel qui se déclenche lorsqu'un utilisateur se connecte. La classe correspondante est Illuminate\Auth\Events\Login. Puisque l'événement existe, on n'a pas à créer une classe Event. Par contre il nous faut une écoute (Listener) :

php artisan make:listener Login

La classe Login se crée ainsi que le dossier :

Voici le code généré :

1. <?php

3. namespace App\Listeners;

5. use Illuminate\Queue\InteractsWithQueue;

6. use Illuminate\Contracts\Queue\ShouldQueue;

8. class Login

9. {

10. /**

11. * Create the event listener.

12. *

13. * @return void

14. */

15. public function __construct()

16. {

17. //

18. }

20. /**

21. * Handle the event.

22. *

23. * @param  object  $event

24. * @return void

25. */

26. public function handle($event)

27. {

28. //

29. }

30. }

On va établir le lien entre l'événement et l'écoute dans le provider EventServiceProvider :

protected $listen = [
'Illuminate\Auth\Events\Login' => ['App\Listeners\Login'],
];

Donc maintenant, dès que l’utilisateur va se connecter, la méthode handle de notre écoute va être appelée. Mais qu'y a-t-il dans la variable $event ? On va faire un petit test :

1. public function handle($event)

2. {

3. dd($event);

4. }

Là, dès qu'on se connecte, on obtient :

1. Login {#218 ▼

2. +user: User {#224 ...}

3. +remember: false

4. }

une instance de la classe Illuminate/Auth/Events/Login avec deux propriétés : une instance du modèle de l'utilisateur et un booléen pour le remember. On aurait aussi pu trouver ces informations en allant voir directement la classe.

Changez ainsi le code :

1. public function handle($event)

2. {

3. dd($event->user->name . " s'est connecté.");

4. }

Cette fois, à la connexion, on obtient quelque chose dans ce genre :

"Durand s'est connecté."

On peut donc ici effectuer tous les traitements qu'on veut, comme comptabiliser les connexions.

I-D - On crée un événement

Maintenant, on va créer un événement et aussi son écoute. On va imaginer qu'on veut savoir quand quelqu'un affiche la page d'accueil. C'est un peu idiot, mais avec le peu qu'on a dans l'application par défaut, on ne va pas faire les difficiles, et de toute façon, c'est pour voir le processus de codage.

On crée l'événement :

php artisan make:event Accueil

On le trouve ici avec la création du dossier :

avec ce code par défaut :

1. <?php

3. namespace App\Events;

5. use Illuminate\Broadcasting\Channel;

6. use Illuminate\Queue\SerializesModels;

7. use Illuminate\Broadcasting\PrivateChannel;

8. use Illuminate\Broadcasting\PresenceChannel;

9. use Illuminate\Foundation\Events\Dispatchable;

10. use Illuminate\Broadcasting\InteractsWithSockets;

11. use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

13. class Accueil

14. {

15. use Dispatchable, InteractsWithSockets, SerializesModels;

17. /**

18. * Create a new event instance.

19. *

20. * @return void

21. */

22. public function __construct()

23. {

24. //

25. }

27. /**

28. * Get the channels the event should broadcast on.

29. *

30. * @return \Illuminate\Broadcasting\Channel|array

31. */

32. public function broadcastOn()

33. {

34. return new PrivateChannel('channel-name');

35. }

36. }

On ne va pas toucher à ce code.

On crée aussi l'écoute :

php artisan make:listener Accueil

et le lien dans le provider EventServiceProvider :

protected $listen = [
'App\Events\Accueil' => ['App\Listeners\Accueil'],
];

Il ne reste plus qu'à déclencher cet événement dans le contrôleur HomeController :

1. public function index()

2. {

3. event(new Accueil);

5. return view('home');

6. }

Maintenant, chaque fois que la page d'accueil est ouverte, l'événement Accueil est déclenché et donc l'écoute Accueil en est informée.

Si on a des informations à transmettre, il suffit de prévoir des propriétés pour l'événement et ça sera transmis à l'écoute.

I-E - L'application d'exemple

Dans l'application d'exemple, on a deux événements avec leur écoute :

La liaison entre événement et écoute est réalisée comme vu plus haut dans le provider EventServiceProvider :

1. protected $listen = [

2. 'App\Events\ModelCreated' => [

3. 'App\Listeners\ModelCreated',

4. ],

5. 'App\Events\PostUpdated' => [

6. 'App\Listeners\PostUpdated',

7. ],

8. ];

I-E-1 - Modèle créé

Le premier événement est déclenché quand un modèle est créé. C'est un événement déjà prévu dans Eloquent. Pour le lier avec notre événement, il faut renseigner dans le modèle la propriété $dispatchesEvents. Par exemple pour le modèle Comment :

protected $dispatchesEvents = [
'created' => ModelCreated::class,
];

Il suffit donc de prévoir ça pour tous les modèles concernés. Comme il s'agit dans l'application de connaître le nombre de nouveaux contacts, articles, utilisateurs et commentaires, on va le prévoir sur tous les modèles correspondants. J'ai déjà évoqué ce point dans le chapitre sur le polymorphisme. J'ai alors expliqué le code du listener :

1. public function handle(EventModelCreated $event)

2. {

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

5. Thumb::makeThumb ($event->model);

6. }

On a vu qu'on mémorisait la nouvelle création dans la table ingoings.

On voit que l'événement transmet une instance du modèle créé (propriété $model) :

1. <?php

3. namespace App\Events;

5. use Illuminate\ {

6. Queue\SerializesModels,

7. Database\Eloquent\Model

8. };

10. class ModelCreated

11. {

12. use SerializesModels;

14. public $model;

16. /**

17. * Create a new event instance.

18. *

19. * @param Model $model

20. */

21. public function __construct(Model $model)

22. {

23. $this->model = $model;

24. }

25. }

On fait autre chose avec la seconde ligne de code dans le listener : on crée une image miniature de celle prévue pour l'article à fin d'affichage dans l'administration :

En effet, il ne serait pas judicieux de charger les images en haute résolution pour ensuite les afficher en miniature. On crée donc des images réduites :

C'est la classe App\Services\Thumb qui est chargée de créer ces miniatures. Sous le capot, c'est le superbe package intervention/image qui est utilisé.

I-E-2 - Article modifié

Le second événement se déclenche lorsqu'un article est modifié. En effet, il est alors possible que l'image de l'article ait été modifiée et il faut donc régénérer la miniature.

Dans le modèle Post, on trouve donc la déclaration des deux événements :

1. protected $dispatchesEvents = [

2. 'created' => ModelCreated::class,

3. 'updated' => PostUpdated::class,

4. ];

et dans le listener PostUpdated, on trouve la régénération éventuelle de l'image :

1. public function handle(EventPostUpdated $event)

2. {

3. if($event->model->wasChanged ('image')) {

4. Thumb::makeThumb ($event->model);

5. }

6. }

Eloquent mémorise les valeurs originales des attributs et la méthode wasChanged permet de savoir si un attribut a été modifié.

I-F - En résumé

- Laravel permet d'utiliser la programmation événementielle.

- Laravel comporte des événements préexistants au niveau de l'authentification et d'Eloquent.

- On peut créer des événements (Events) et des écoutes (Listeners), et les mettre en relation.

II - La localisation

Lorsqu'on crée un site, c'est souvent dans une optique multilangages. On parle alors d'internationalisation (i18n) et de localisation (L10n). L'internationalisation consiste à préparer une application pour la rendre potentiellement adaptée à différents langages. La localisation consiste, quant à elle, à ajouter un composant spécifique à une langue.

C'est un sujet assez complexe qui ne se limite pas à la traduction des textes, mais qui impacte aussi la représentation des dates, la gestion des pluriels, parfois la mise en page…

Qu'a à nous proposer Laravel dans ce domaine ? Nous allons le voir dans ce chapitre.

i18n parce qu'il y a 18 lettres entre le « i » et le « n » de internationalisation et L10n parce qu'il y a 10 lettres entre le « L » et le « n » de Localisation.

II-A - Le principe

II-A-1 - Les fichiers de langage

On a deux façons de gérer les langues.

II-A-1-a - Clés et valeurs

On a déjà parlé des fichiers de langage. Lorsque Laravel est installé, on a cette architecture :

Il n'est prévu au départ que la langue anglaise (en) avec quatre fichiers. Pour ajouter une langue, il suffit de créer un nouveau dossier : par exemple fr pour le Français.

Il faut évidemment avoir le même contenu comme nous allons le voir.

Vous n'allez pas être obligé de faire toute cette traduction vous-même ! Certains s'en sont déjà chargés avec ce package. Nous l'avons déjà utilisé dans ce cours.

L'application d'exemple embarque deux langues : anglais et français :

Les fichiers présents sont tous constitués exactement de la même manière. Prenons le plus léger : pagination.php. Pour l'anglais on a :

1. return [

2. 'previous' => '&laquo; Previous',

3. 'next' => 'Next &raquo;',

4. ];

On voit qu'on se contente de renvoyer un tableau avec des clés et des valeurs. On ne peut pas faire plus simple ! Si on prend le même fichier en version française :

1. return [

2. 'previous' => '&laquo; Précédent',

3. 'next'     => 'Suivant &raquo;',

4. ];

on retrouve évidemment les mêmes clés avec des valeurs adaptées au langage.

II-A-1-b - Les textes

Le système clé/valeur peut devenir rapidement lourd et confus lorsqu'on a beaucoup de textes à traduire. Laravel propose une autre approche qui consiste à utiliser la traduction par défaut comme clé.

Si la langue par défaut est l'anglais, par exemple, on aura le texte anglais directement dans le code. Les autres traductions se trouveront dans des fichiers JSON.

Si vous regardez au niveau de l'application d'exemple, vous trouvez un fichier fr.json :

Ce fichier contient des éléments de ce type :

"All": "Tout",

On a :

- All : le texte en langage par défaut présent dans le code ;

- Tout : la traduction à prévoir en français.

Les deux systèmes peuvent cohabiter et se compléter, c'est d'ailleurs le cas dans l'application d'exemple.

II-A-2 - Configuration de la locale

La configuration de la « locale » est effectuée dans le fichier config/app.php :

'locale' => 'en',

On peut changer cette locale en cours d'exécution si nécessaire :

App::setLocale('fr');

Si une traduction n'est pas trouvée dans la locale actuelle, alors on va chercher la traduction de la locale par défaut définie également dans le fichier config/app.php :

'fallback_locale' => 'en',

On peut connaître la locale actuelle :

$locale = App::getLocale();

et même tester si on a une certaine locale :

if (App::isLocale('fr')) {
//
}

II-A-3 - Dans le code

II-A-3-a - L'helper __

Concrètement, au niveau du code, on utilise « l'helper __ ». Il suffit de préciser la clé comme paramètre (la clé pour le système clé-valeur et le texte dans la langue d'origine pour le second système). On trouve par exemple ce code dans le contrôleur Front/PostController de l'application d'exemple :

$info = __('Posts for category: ') . '<strong>' . $category->title . '</strong>';

Si la locale est en, c'est ce texte qui sera directement affiché. Si la locale est fr, c'est le texte correspondant qu'on trouve dans le fichier fr.json qui sera affiché :

"Posts for category: ": "Articles pour la catégorie : ",

II-A-3-b - Blade

On peut utiliser l'helper __ dans une vue « Blade » :

{{ __('Get In Touch With Us') }}

mais il est plus pratique d'utiliser la directive @lang :

@lang('Get In Touch With Us')

II-B - Le pluriel

La gestion du pluriel n'est pas toujours facile. On peut prévoir le texte du singulier et celui du pluriel séparés par le caractère « | » :

'pommes' => 'une pomme|plusieurs pommes',

On peut affiner l'affaire :

'pommes' => '{0} aucune pomme|[1,10] quelques pommes|[10,*] beaucoup de pommes',

On utilise dans le code la fonction trans_choice. Voici un cas tiré de l'application d'exemple :

{{ trans_choice(__('comment|comments'), $post->valid_comments_count) }}

Selon la valeur de $post->valid_comments_count, autrement dit le nombre de commentaires valides, on prend le singulier ou le pluriel grâce à trans_choice et dans la langue voulue grâce à l'helper __.

Si on a un seul commentaire, on a le singulier :

sinon, on a le pluriel :

II-C - Des assistants

Pour gérer toutes ces traductions, il existe des assistants bien utiles.

Pour le système clé-valeur, voici un package intéressant :

Il permet pas mal de manipulations en ligne de commande mais se limite au premier système (clé-valeur).

Pour la gestion des fichiers « JSON » j'ai créé un package :

Il ajoute quatre commandes à Artisan :

- language:strings pour visualiser tous les textes du code (dans app et resource/views) ;

- language:make pour créer un fichier JSON pour une locale complétée avec les textes du code ;

- language:diff pour voir les différences entre une locale JSON et les textes du code ;

- language:sync pour synchroniser une locale JSON avec les textes du code.

Ce package est installé dans l'application d'exemple, vous pouvez donc directement l'utiliser.

Une approche intéressante est celle de Langly qui se présente comme une application externe :

II-D - Les noms pour la validation

Il y a un autre élément à prendre en compte pour la traduction : c’est le nom des contrôles de saisie. Ces noms ne sont pas visibles tant qu'il n'y a pas de souci de validation. Ils sont alors transmis dans le texte de l'erreur.

‌On voit mal arriver en langue française : « Le champ name est obligatoire. ». Alors comment faire ?

Si vous regardez dans le fichier resources/lang/fr/validation.php, vous trouverez ce tableau :

'attributes'           => [
'name'                  => 'Nom',
'username'              => 'Pseudo'

Ici on fait correspondre le nom d'un attribut avec sa version linguistique pour justement avoir quelque chose de cohérent au niveau du message d'erreur.

II-E - Les dates

Il ne vous a sans doute pas échappé que les dates et les heures ne sont pas présentées de la même manière selon la langue utilisée, en particulier entre le français et l'anglais.

En France nous utilisons le format jj/mm/aaaa alors que les Américains utilisent le format mm/jj/aaaa. De la même manière les heures sont présentées différemment.

C'est le cas dans l'application d'exemple. Cette écriture en français :

donnera ceci en anglais :

Comment cela est-il réalisé ?

Laravel utilise Carbon pour la gestion des dates. Carbon hérite de la classe PHP DateTime. On dispose donc de toutes les possibilités de cette classe plus toutes celles ajoutées par Carbon, et elles sont nombreuses.

Quand Eloquent récupère une date de created_at ou updated_at, il la transforme en instance Carbon. On peut modifier ce comportement, mais je n'en vois pas l'intérêt sauf cas bien particulier.

Comme la classe DataTime ne comporte pas de localisation, Carbon ajoute la méthode formatLocalized($format) , mais pour que ça fonctionne, il faut d'abord fixer la locale avec setlocale(). Si vous regardez dans le fichier AppServiceProvider de l'application d'exemple, vous trouvez ce code :

1. public function boot()

2. {

3. setlocale(LC_TIME, config('app.locale'));

5. ...

6. }

Ensuite, dans la vue back/posts/table.blade.php :

<td>{{ $post->created_at->formatLocalized('%c') }}</td>

la méthode formatLocalized fait appel à la fonction PHP strftime. C'est ainsi qu'on a la date et l'heure bien affichées pour l'anglais et le français.

Dans la vue front/post.blade.php, on trouve :

<li class="date">{{ $post->created_at->formatLocalized('%A %d %B %Y') }}</li>

C'est le même principe avec une autre présentation de la date.

Le but de ce chapitre est de montrer comment localiser l'interface d'une page web. S'il s'agit d'adapter le contenu selon la langue, c'est une autre histoire et il faut alors intervenir au niveau de l'URL. En effet, un principe fondamental du web veut que pour une certaine URL on renvoie toujours le même contenu.

II-F - En résumé

- Laravel possède les outils de base pour la localisation.

- Laravel propose deux systèmes complémentaires : clé-valeur ou texte dans le code associé à des fichiers JSON.

- Il faut créer autant de fichiers de localisation qu'on a de langues à traiter.

- Il faut prévoir la version linguistique des attributs.

- Grâce à Carbon, la présentation des dates et heures selon la locale est simplifiée.

III - Les notifications

On a vu dans ce cours comment envoyer un e-mail avec Laravel. Mais on dispose aussi d'un système complet de notifications, par exemple par SMS, qui inclue aussi les e-mails ou même la base de données.

Classiquement une notification est un message court pour informer un utilisateur qu'il s'est passé quelque chose qui le concerne dans l'application.

Par exemple, une donnée sensible a été mise à jour : on envoie un SMS par sécurité en informant l'utilisateur de ce changement et, si ce n'est pas lui qui l'a effectué, il peut alors intervenir.

Évidemment, pour tout ce qui n'est pas e-mail ou base de données, il faut utiliser un service externe. Il y a un site dédié pour tous les drivers existants et la liste est déjà longue !

L'application d'exemple utilise les notifications concernant l'envoi d'e-mail pour :

- la confirmation de l'adresse e-mail lors de l'enregistrement (par l'intermédiaire d'un package) ;

- le renouvellement du mot de passe.

Elle utilise également les notifications en base de données pour prévenir les rédacteurs que de nouveaux commentaires ont été ajoutés à leur article.

III-A - Organisation du code

Les notifications se trouvent dans le dossier app/Notifications :

Ce dossier n'existe pas dans l'installation de base de Laravel. Il est ajouté lorsqu'on crée la première notification avec Artisan qui dispose à cet effet d'une commande :

Évidemment, pour les packages, les notifications se situent dans un dossier du package, par exemple :

III-B - Le renouvellement du mot de passe

On a vu en détail le renouvellement du mot de passe dans ce chapitre. J'ai alors précisé qu'on envoyait un e-mail par le système de notification en indiquant que je vous en parlerai plus tard. C'est ce que je vais faire maintenant.

On a vu que Laravel, après la demande de renouvellement de l'utilisateur, expédie ce genre d’e-mail :

Par défaut, la classe de notification se situe dans le framework :

Avec ce code :

1. <?php

3. namespace Illuminate\Auth\Notifications;

5. use Illuminate\Notifications\Notification;

6. use Illuminate\Notifications\Messages\MailMessage;

8. class ResetPassword extends Notification

9. {

10. /**

11. * The password reset token.

12. *

13. * @var string

14. */

15. public $token;

17. /**

18. * Create a notification instance.

19. *

20. * @param  string  $token

21. * @return void

22. */

23. public function __construct($token)

24. {

25. $this->token = $token;

26. }

28. /**

29. * Get the notification's channels.

30. *

31. * @param  mixed  $notifiable

32. * @return array|string

33. */

34. public function via($notifiable)

35. {

36. return ['mail'];

37. }

39. /**

40. * Build the mail representation of the notification.

41. *

42. * @param  mixed  $notifiable

43. * @return \Illuminate\Notifications\Messages\MailMessage

44. */

45. public function toMail($notifiable)

46. {

47. return (new MailMessage)

48. ->line('You are receiving this email because we received a password reset request for your account.')

49. ->action('Reset Password', url(config('app.url').route('password.reset', $this->token, false)))

50. ->line('If you did not request a password reset, no further action is required.');

51. }

52. }

Comme c'est une notification par e-mail, on a une méthode toMail. D'autre part on précise dans la méthode via qu'on retourne un e-mail. La classe MailMessage offre un certain nombre de méthodes bien pratiques comme :

- line : pour écrire une ligne ;

- action : pour afficher un bouton ;

- from : pour définir l'adresse de l'expéditeur ;

- subject : pour définir le sujet ;

- cc et bcc : pour faire des copies ;

- attach : pour joindre un document ;

- priority : pour fixer la priorité…

Vous constatez qu'on a une mise en forme de l'e-mail plutôt esthétique. Cette mise en forme est issue d'une vue par défaut dans le framework :

Si vous voulez modifier cette mise en forme, il faut publier cette vue :

php artisan vendor:publish --tag=laravel-notifications

Vous retrouvez alors la vue dans le dossier resources/views/vendor/notifications.

Vous pouvez vous demander maintenant comment est effectivement commandée cette notification. Ça se passe dans le trait CanResetPassword avec la méthode notify :

1. <?php

3. namespace Illuminate\Auth\Passwords;

5. use Illuminate\Auth\Notifications\ResetPassword as ResetPasswordNotification;

7. trait CanResetPassword

8. {

9. ...

11. public function sendPasswordResetNotification($token)

12. {

13. $this->notify(new ResetPasswordNotification($token));

14. }

15. }

Dans l'application d'exemple, on n'utilise pas directement la classe de notification prévue dans le framework parce qu'on veut localiser le texte :

1. public function toMail()

2. {

3. return (new MailMessage)

4. ->line(__('You are receiving this email because we received a password reset request for your account.'))

5. ->line(__('Click the button below to reset your password:'))

6. ->action(__('Reset Password'), url('password/reset', $this->token))

7. ->line(__('If you did not request a password reset, no further action is required.'));

8. }

On a donc cette classe dans l'application :

et pour informer Laravel d'avoir à utiliser cette classe, on surcharge la méthode sendPasswordResetNotification qu'on a vue ci-dessus, présente dans le trait CanResetPassword au niveau de notre modèle User :

1. <?php

3. ...

5. use App\Notifications\ResetPassword as ResetPasswordNotification;

7. class User extends Authenticatable

8. {

9. use Notifiable, IngoingTrait;

11. ...

13. public function sendPasswordResetNotification($token)

14. {

15. $this->notify(new ResetPasswordNotification($token));

16. }

18. ...

19. }

III-C - La notification des rédacteurs pour les nouveaux commentaires

On peut utiliser les notifications en base de données. Elles sont ainsi stockées en attendant d'être lues.

III-C-1 - La table des notifications

Pour que ça fonctionne, il faut ajouter une table avec cette commande d'Artisan :

php artisan notifications:table

ce qui a pour effet d'ajouter une migration :

Après avoir effectué la migration, on se retrouve avec la table « notifications » :

C'est dans cette table que sont stockées les notifications.

III-C-2 - La notification

Comme pour les e-mails, il nous faut une classe de notification :

1. <?php

3. namespace App\Notifications;

5. use Illuminate\Notifications\Notification;

6. use App\Models\Post;

8. class Commented extends Notification

9. {

10. /**

11. * Post property.

12. *

13. * @var \App\Models\Post

14. */

15. protected $post;

17. /**

18. * User id property.

19. *

20. * @var integer

21. */

22. protected $user_id;

24. /**

25. * Create a new notification instance.

26. *

27. * @param Post $post

28. * @param integer $user_id

29. */

30. public function __construct(Post $post, $user_id)

31. {

32. $this->post = $post;

33. $this->user_id = $user_id;

34. }

36. /**

37. * Get the notification's delivery channels.

38. *

39. * @param  mixed  $notifiable

40. * @return array

41. */

42. public function via($notifiable)

43. {

44. return ['database'];

45. }

47. /**

48. * Get the array representation of the notification.

49. *

50. * @param  mixed  $notifiable

51. * @return array

52. */

53. public function toArray($notifiable)

54. {

55. return [

56. 'title' => $this->post->title,

57. 'slug' => $this->post->slug,

58. 'user_id' => $this->user_id,

59. ];

60. }

61. }

cette fois, dans la méthode via, on a database.

On voit qu'on doit transmettre dans le constructeur une instance de l'article concerné ($post) ainsi que l'identifiant de l'auteur (user_id). La mise en forme se fait dans la méthode toArray où on retourne un tableau de données qui sera transformé en JSON dans la base.

On va ainsi transmettre le titre, le « slug » (pour générer l'URL) de l'article, ainsi que l'identifiant de l'auteur.

III-C-3 - Création d'une notification

Une notification sera créée quand un commentaire le sera. Voici la méthode store du contrôleur des commentaires (App\Http\Controllers\Front\CommentController) :

1. public function store(CommentRequest $request, Post $post, $comment_id = null)

2. {

3. Comment::create ([

4. 'body' => $request->input('message' . $comment_id),

5. 'post_id' => $post->id,

6. 'user_id' => $request->user()->id,

7. 'parent_id' => $comment_id,

8. ]);

10. $post->user->notify(new Commented($post, $request->user()->id));

12. if (!$request->user()->valid) {

13. $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.'));

14. }

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

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

18. }

20. return back();

21. }

La ligne qui crée la notification est celle-ci :

$post->user->notify(new Commented($post, $request->user()->id));

exactement comme on l'a vu pour les e-mails ci-dessus.

On trouve cet enregistrement dans la table des notifications :

La clé étrangère qui permet de connaître l'utilisateur est « notifiable_id ».

On a le type de notification pour les distinguer si on en a plusieurs, ce qui n'est pas le cas de l'application d'exemple. La colonne read_at est à NULL parce que la notification n'a pas encore été lue. Dans la colonne data on retrouve sous forme de JSON les informations transmises (title, slug et user_id).

III-C-4 - Les routes et le contrôleur

On a deux routes :

La première est pour l'affichage des notifications.

La seconde est pour marquer une notification comme lue.

Voici le contrôleur (App\Http\Controllers\Back\NotificationController) :

1. <?php

3. namespace App\Http\Controllers\Back;

5. use App\ {

6. Models\User,

7. Http\Controllers\Controller

8. };

9. use Illuminate\ {

10. Http\Request,

11. Notifications\DatabaseNotification

12. };

14. class NotificationController extends Controller

15. {

16. /**

17. * Display a listing of the resource.

18. *

19. * @param  \App\Models\User  $user

20. * @return \Illuminate\Http\Response

21. */

22. public function index(User $user)

23. {

24. return view('back.notifications.index', compact('user'));

25. }

27. /**

28. * Update the specified resource in storage.

29. *

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

31. * @param  \Illuminate\Notifications\DatabaseNotification $notification

32. * @return \Illuminate\Http\Response

33. * @internal param int $id

34. */

35. public function update(Request $request, DatabaseNotification $notification)

36. {

37. $notification->markAsRead();

39. if($request->user()->unreadNotifications->isEmpty()) {

40. return redirect()->route('posts.index');

41. }

43. return back();

44. }

45. }

La première méthode (index) ne pose pas de problème puisqu'on se contente d'utiliser une vue en transmettant une instance du modèle pour l'utilisateur connecté.

La seconde méthode (update) est destinée à marquer une notification comme lue, ce qui se fait avec la méthode markAsRead.

Remarquez que pour les deux méthodes, on utilise la liaison automatique entre le paramètre de la route et un modèle. De cette manière on a directement une instance du modèle dans la méthode.

III-C-5 - La vue index

Voici la vue pour afficher les notifications (resources/views/back/notifications/index.blade.php) :

1. @extends('back.layout')

3. @section('main')

5. <div class="row">

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

7. <div class="box">

8. <div class="box-body table-responsive">

9. <table class="table table-striped table-bordered">

10. <thead>

11. <tr>

12. <th>@lang('Post')</th>

13. <th>@lang('Author')</th>

14. <th>@lang('Date')</th>

15. <th>@lang('Valid')</th>

16. <th></th>

17. </tr>

18. </thead>

19. <tfoot>

20. <tr>

21. <th>@lang('Post')</th>

22. <th>@lang('Author')</th>

23. <th>@lang('Date')</th>

24. <th>@lang('Valid')</th>

25. <th></th>

26. </tr>

27. </tfoot>

28. <tbody>

29. @foreach ($user->unreadNotifications as $notification)

30. <tr>

31. @php $user = user($notification->data['user_id']) @endphp

32. <td><a href="{{ route('posts.display', [$notification->data['slug']]) }}">{{ $notification->data['title'] }}</a></td>

33. <td>{{ $user->name }}</td>

34. <td>{{ $notification->created_at->formatLocalized('%c') }}</td>

35. <td><input type="checkbox" name="valid" {{ $user->valid ? 'checked' : '' }} disabled></td>

36. <td>

37. <form action="{{ route('notifications.update', [$notification->id]) }}" method="POST">

38. {{ csrf_field() }}

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

40. <input type="submit" class="btn btn-success btn-xs btn-block" value="@lang('Mark as read')">

41. </form>

42. </td>

43. </tr>

44. @endforeach

45. </tbody>

46. </table>

47. </div>

48. </div>

49. <!-- /.box -->

50. </div>

51. <!-- /.col -->

52. </div>

53. <!-- /.row -->

55. @endsection

On voit qu'on peut récupérer toutes les notifications non lues de l'utilisateur avec $user->unreadNotifications.

Voici l'aspect de la vue :

Si on clique sur « Marquer comme lu », on appelle la méthode update du contrôleur vue ci-dessus et la notification est marquée comme lue (mise à jour de la colonne read_at). Elle disparaît de l'affichage.

Il y aurait encore beaucoup à dire sur les notifications au-delà des deux exemples de ce chapitre : reportez-vous à la documentation officielle pour en savoir plus.

III-D - En résumé

- Laravel comporte un système complet et performant de notifications.

- On peut envoyer des e-mails avec les notifications, leur mise en forme est facilitée par la présence de puissantes méthodes et d'un template qu'on peut personnaliser.

- On peut stocker les notifications en base de données pour une utilisation ultérieure.

IV - La sécurité

Lorsqu'on développe une application, on prend plein de précautions : par exemple les utilisateurs doivent s'authentifier pour éviter des actions non autorisées. Dans le code, on peut vérifier si la personne est authentifiée et quel est son degré d'habilitation.

Mais en dehors de l'authentification, on doit gérer certaines situations comme savoir si un utilisateur authentifié a le droit de modifier une ressource particulière. Laravel nous offre un système d'autorisations bien pratique.

IV-A - L'authentification

IV-A-1 - Les middlewares

On a déjà vu l'authentification en détail dans un précédent chapitre. On a aussi vu que, dans l'application d'exemple, les utilisateurs authentifiés ont un rôle selon leur degré d'habilitation. On peut filtrer les accès selon le statut avec des middlewares. Dans l'application d'exemple on a le middleware Admin :

avec ce code :

1. <?php

3. namespace App\Http\Middleware;

5. use Closure;

7. class Admin

8. {

9. /**

10. * Handle an incoming request.

11. *

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

13. * @param  \Closure  $next

14. * @return mixed

15. */

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

17. {

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

20. if ($user && $user->role === 'admin') {

21. return $next($request);

22. }

24. return redirect()->route('home');

25. }

26. }

Dans la méthode handle, on récupère l'utilisateur en cours ($request->user()) et on teste pour savoir s'il s'agit d'un administrateur ($user->role === 'admin').

On peut ainsi filtrer les routes réservées aux administrateurs :

Route::middleware('admin')->group(function () {
...
}

On a de la même manière le middleware Redac pour les rédacteurs avec le même type de code :

IV-A-2 - Le throttle

D'autre part est prévue par défaut dans l'authentification une sécurisation de la connexion contre les attaques « brute force ». Si vous regardez dans le trait Illuminate\Foundation\Auth\AuthenticatesUsers, vous trouvez le trait ThrottlesLogins :

trait AuthenticatesUsers
{
use RedirectsUsers, ThrottlesLogins;

C'est ce trait qui assure la sécurité. À partir d'un certain nombre de connexions avortées (cinq par défaut), on bloque l'accès pour le couple « e-mail (ou autre) / adresse IP » pendant une minute.

IV-A-3 - Le hachage

Laravel possède la façade Hash qui permet de faire du hachage avec Bcrypt. C'est ce qui est utilisé pour l'encryption du mot de passe dans AuthR/egisterController avec l'helper bcrypt :

1. protected function create(array $data)

2. {

3. return User::create([

4. ...

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

6. ]);

7. }

Si vous utilisez l'authentification par défaut de Laravel, vous n'avez pas besoin d'en savoir davantage parce que tout est géré par le framework.

IV-B - Quelques éléments à prendre en considération

IV-B-1 - Les injections SQL

Une injection SQL est une attaque qui consiste à ajouter à une requête inoffensive, un complément qui peut être dangereux pour l'application.

Si vous utilisez Eloquent vous êtes automatiquement immunisé contre ces attaques.

Par contre, si vous faites des requêtes avec par exemple DB::raw() ou whereRaw, autrement dit si vous contournez Eloquent, alors vous devez vous-même vous prémunir contre les attaques SQL.

IV-B-2 - CSRF

Je vous ai déjà parlé de la protection CSRF dans ce cours : elle est automatiquement mise en place par Laravel.

IV-B-3 - XSS (Cross-Site Scripting)

Dans ce cas, c'est une injection insidieuse de HTML ou de « JavaScript ». Laravel ne fait pas de nettoyage des entrées en dehors de la validation. Si vous voulez en faire un, libre à vous, vous pouvez le faire par exemple avec ce composant. Par contre vous avez avec Blade la syntaxe sécuritaire {{ ... }} qui « échappe » les données à l'affichage.

Faites très attention avec la syntaxe {!! … !!} ! En gros, vous devez l'éviter avec des données issues de l'extérieur de l'application.

IV-B-4 - L'assignation de masse

Je vous ai déjà parlé de l'assignation de masse. Vous devez choisir avec soin les colonnes des tables qui sont sans danger dans une mise à jour de masse, celles que vous mettez dans la propriété $fillable (ou $guarded avec la logique inverse) d'un modèle.

IV-B-5 - Les requêtes de formulaire

Une autre possibilité pour sécuriser l'application existe au niveau des requêtes de formulaires. On a vu qu'il y a une méthode authorize. Dans l'application d'exemple cette méthode n'est pas utilisée, mais il faut renvoyer true pour que ça fonctionne. C'est pour cette raison que toutes les requêtes de formulaires héritent de cette classe parente pour éviter d'avoir du code dupliqué :

1. <?php

3. namespace App\Http\Requests;

5. use Illuminate\Foundation\Http\FormRequest;

7. abstract class Request extends FormRequest

8. {

9. /**

10. * Authorization

11. *

12. * @return boolean

13. */

14. public function authorize()

15. {

16. return true;

17. }

18. }

Vous pouvez ici vérifier, par exemple, si un utilisateur a le droit d'utiliser effectivement le formulaire, et renvoyer false si ce n'est pas le cas.

IV-C - Les autorisations

En plus de ces possibilités, Laravel nous offre un système complet d'autorisations.

On dispose d'une commande pour créer une autorisation :

php artisan make:policy TestPolicy

avec ce code par défaut :

1. <?php

3. namespace App\Policies;

5. use App\User;

6. use Illuminate\Auth\Access\HandlesAuthorization;

8. class TestPolicy

9. {

10. use HandlesAuthorization;

12. /**

13. * Create a new policy instance.

14. *

15. * @return void

16. */

17. public function __construct()

18. {

19. //

20. }

21. }

Le plus simple pour voir comment cela fonctionne est de prendre un exemple. Dans l'application d'exemple, on a le dossier app/Policies avec trois fichiers :

Voyons le code de PostPolicy :

1. <?php

3. namespace App\Policies;

5. use App\Models\ {User, Post};

7. class PostPolicy extends Policy

8. {

9. /**

10. * Determine whether the user can manage the post.

11. *

12. * @param  \App\Models\User  $user

13. * @param  \App\Models\Post  $post

14. * @return mixed

15. */

16. public function manage(User $user, Post $post)

17. {

18. return $user->id === $post->user_id;

19. }

20. }

On voit que la classe étend une autre classe dans le même dossier :

1. <?php

3. namespace App\Policies;

5. use App\Models\User;

6. use Illuminate\Auth\Access\HandlesAuthorization;

8. class Policy

9. {

10. use HandlesAuthorization;

12. /**

13. * Grant all abilities to administrator.

14. *

15. * @param  \App\Models\User  $user

16. * @return bool

17. */

18. public function before(User $user)

19. {

20. if ($user->role === 'admin') {

21. return true;

22. }

23. }

24. }

La méthode before est la première à être appelée. À ce niveau on vérifie si l'utilisateur est un administrateur, et donc possède tous les droits : si c'est le cas, on renvoie true.

On a ensuite la méthode manage avec comme paramètres l'utilisateur ($user) et l'article ($post). Si l'utilisateur est le créateur de l'article, on renvoie true.

Maintenant que cette autorisation est en place, il faut la déclarer. Regardez le fichier app/Providers/AuthServiceProvider :

1. <?php

3. namespace App\Providers;

5. use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

6. use App\Models\Post;

7. use App\Policies\PostPolicy;

8. use App\Models\Comment;

9. use App\Policies\CommentPolicy;

11. class AuthServiceProvider extends ServiceProvider

12. {

13. /**

14. * The policy mappings for the application.

15. *

16. * @var array

17. */

18. protected $policies = [

19. Post::class => PostPolicy::class,

20. Comment::class => CommentPolicy::class,

21. ];

23. /**

24. * Register any authentication / authorization services.

25. *

26. * @return void

27. */

28. public function boot()

29. {

30. $this->registerPolicies();

31. }

32. }

Dans la propriété policies, on a prévu d'ajouter la classe PostPolicy.

Il ne nous reste plus qu'à voir comment on l'utilise.

Par défaut le contrôleur de base utilise le trait AuthorizesRequests :

1. <?php

3. namespace App\Http\Controllers;

5. use Illuminate\Routing\Controller as BaseController;

6. use Illuminate\Foundation\Bus\DispatchesJobs;

7. use Illuminate\Foundation\Validation\ValidatesRequests;

8. use Illuminate\Foundation\Auth\Access\AuthorizesRequests;

10. class Controller extends BaseController

11. {

12. use AuthorizesRequests, DispatchesJobs, ValidatesRequests;

13. }

Donc tous les contrôleurs sont au courant de l'existence des autorisations enregistrées.

Regardez ces deux méthodes du contrôleur Back/PostController :

1. public function edit(Post $post)

2. {

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

5. $categories = Category::all()->pluck('title', 'id');

7. return view('back.posts.edit', compact('post', 'categories'));

8. }

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

11. {

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

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

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

17. }

Elles sont destinées, pour la première, à afficher le formulaire de modification d'un article, et pour la seconde, à traiter la soumission des modifications.

Dans les deux cas on met en œuvre l'autorisation :

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

Donc s'il ne s'agit pas d'un administrateur ou du propriétaire de l'article, ça va coincer ici.

On peut également utiliser l'autorisation en tant que middleware dans les routes (can) :

Route::name('posts.seen')->put('posts/seen/{post}', 'PostController@updateSeen')->middleware('can:manage,post');
Route::name('posts.active')->put('posts/active/{post}/{status?}', 'PostController@updateActive')->middleware('can:manage,post');

On peut aussi utiliser une autorisation dans une vue. Ce n'est pas utilisé dans l'application d'exemple, mais on aurait pu écrire :

@if (Auth::user()->can('manage', $post))

Vous pouvez trouver la documentation complète ici.

IV-D - En résumé

- L'authentification permet de sécuriser une application et d'adapter l'affichage selon le degré d'habilitation de l'utilisateur.

- Eloquent immunise contre les injections SQL.

- La protection CSRF est automatiquement mise en place par Laravel.

- Il faut être très prudent avec la syntaxe {!! … !!} de Blade.

- On peut sécuriser les requêtes de formulaire avec leur méthode authorize.

- Laravel comporte un système complet et simple de gestion des autorisations (policies).

V - Ajax

« Ajax » est une technologie JavaScript fort répandue qui permet d'envoyer des requêtes au serveur et de recevoir des réponses sans rechargement de la page. Il est par ce moyen possible de modifier dynamiquement le « DOM », donc une partie de la page.

Dans ce chapitre nous allons voir comment mettre en œuvre Ajax avec Laravel en prenant un cas de l'application d'exemple.

V-A - Les messages dans l'application

Pour les utilisateurs autres que rédacteurs et administrateurs, il y a la possibilité de laisser un message avec un formulaire :

Lorsqu'un administrateur se connecte et va dans le panneau d'administration, il a une page pour ces messages :

L'administrateur peut voir les nouveaux messages grâce à la case à cocher « Nouveau ». Il peut changer le statut en décochant la case.

Il est évident qu'il serait dommage de recharger la page pour ça alors qu'il suffit d'informer le serveur avec une requête Ajax.

Pour améliorer l'ergonomie, on a aussi prévu une petite animation le temps de l'échange de requêtes :

Il faut aussi gérer cette animation avec JavaScript.

Mais il faut distinguer deux cas :

- la case à cocher Nouveau est cochée :

Dans ce cas, si on change le statut d'un message, il n'est plus nouveau et donc il doit disparaître. Du coup, ça change la pagination et il faut recharger la page.

- la case à cocher Nouveau est décochée :

Dans ce cas, au changement de statut, le message reste affiché et il n'y a rien à faire de spécial si ce n'est gérer l'animation.

V-B - La vue des messages

Dans les vues, on en a deux qui concernent l'affichage des messages dans l'administration :

Dans la vue table.blade.php, on a le code chargé de l'affichage du tableau des messages :

1. @foreach($contacts as $contact)

2. <div class="box">

3. <div class="box-body table-responsive">

4. <table id="contacts" class="table table-striped table-bordered">

5. <thead>

6. <tr>

7. <th>@lang('Name')</th>

8. <th>@lang('Email')</th>

9. <th>@lang('New')</th>

10. <th>@lang('Creation')</th>

11. <th></th>

12. </tr>

13. </thead>

14. <tbody>

15. <tr>

16. <td>{{ $contact->name }}</td>

17. <td>{{ $contact->email }}</td>

18. <td>

19. <input type="checkbox" name="seen" value="{{ $contact->id }}" {{ is_null($contact->ingoing) ?  'disabled' : 'checked'}}>

20. </td>

21. <td>{{ $contact->created_at->formatLocalized('%c') }}</td>

22. <td><a class="btn btn-danger btn-xs btn-block" href="{{ route('contacts.destroy', [$contact->id]) }}" role="button" title="@lang('Destroy')"><span class="fa fa-remove"></span></a></td>

23. </tr>

24. </tbody>

25. </table>

26. </div>

27. <!-- /.box-body -->

28. <div id="message" class="box-footer">

29. {{ $contact->message }}

30. </div>

31. </div>

32. <!-- /.box -->

33. @endforeach

On a une boucle pour passer en revue tous les messages :

1. @foreach($contacts as $contact)

2. ...

3. @endforeach

Voici le code pour les cases à cocher :

<input type="checkbox" name="seen" value="{{ $contact->id }}" {{ is_null($contact->ingoing) ?  'disabled' : 'checked'}}>

La case est cochée si on trouve un enregistrement en relation dans la table ingoings. On a vu cet aspect de l'application avec cette table qui permet de connaître les nouveaux éléments :

Les cases sont distinguées grâce à l'attribut value qui contient l'identifiant du message.

V-C - Le JavaScript

Dans la vue index.blade.php, on trouve le JavaScript pour gérer Ajax :

1. var contact = (function () {

3. var url = '{{ route("contacts.index") }}'

4. var swalTitle = '@lang("Really destroy contact ?")'

5. var confirmButtonText = '@lang("Yes")'

6. var cancelButtonText = '@lang("No")'

7. var errorAjax = '@lang("Looks like there is a server issue...")'

9. var onReady = function () {

10. $('#pagination').on('click', 'ul.pagination a', function (event) {

11. back.pagination(event, $(this), errorAjax)

12. })

13. $('#pannel').on('change', ':checkbox[name="seen"]', function () {

14. back.seen(url, $(this), errorAjax)

15. })

16. .on('click', 'td a.btn-danger', function (event) {

17. back.destroy(event, $(this), url, swalTitle, confirmButtonText, cancelButtonText, errorAjax)

18. })

19. $('.box-header :radio, .box-header :checkbox').click(function () {

20. back.filters(url, errorAjax)

21. })

22. }

24. return {

25. onReady: onReady

26. }

28. })();

30. $(document).ready(contact.onReady)

Dans l'application, le JavaScript est organisé de façon modulaire. Les variables et les fonctions sont privées et on expose selon les besoins avec une API.

On détecte les changements sur les cases à cocher qui s'appellent seen :

$('#pannel').on('change', ':checkbox[name="seen"]', function () {

On appelle alors une méthode :

back.seen(url, $(this), errorAjax)

La valeur de l'URL est donnée par cette ligne de code :

var url = '{{ route('contacts.index') }}'

donc admin/contacts.

Pour l'application, l'essentiel du JavaScript pour l'administration est rassemblé dans le fichier adminlte/js/back.js. On trouve donc son chargement dans la vue :

<script src="/adminlte/js/back.js"></script>

On trouve dans ce fichier la méthode seen :

1. var seen = function (url, that, errorAjax) {

2. var urlSeen = url + '/seen/' + that.val()

3. // If "new" is checked we must reload the page

4. if(getCheckboxValueByName('new')) {

5. ajax(urlSeen, 'PUT', url, errorAjax)

6. } else {

7. ajaxNoLoad(urlSeen, 'PUT', errorAjax, that)

8. }

9. }

On trouve les deux cas évoqués ci-dessus selon l'état de la case à cocher Nouveau.

Si elle est cochée, il est fait appel à la méthode ajax :

1. var ajax = function (target, verb, url, errorAjax) {

2. spin()

3. $.ajax({

4. url: target,

5. type: verb

6. })

7. .done(function () {

8. load(url, errorAjax)

9. })

10. .fail(function () {

11. fail(errorAjax)

12. }

13. )

14. }

On voit qu'ici, au retour, on recharge la page :

load(url, errorAjax)

Si elle est décochée, on fait appel à la méthode ajaxNoLoad :

1. var ajaxNoLoad = function (target, verb, errorAjax, that) {

2. spin()

3. $.ajax({

4. url: target,

5. type: verb

6. })

7. .done(function () {

8. unSpin()

9. that.prop('disabled', true)

10. })

11. .fail(function () {

12. fail(errorAjax)

13. })

14. }

Ici, au retour :

- on arrête l'animation (unSpin) ;

- on rend la case à cocher inactive (that.prop('disabled', true)). On ne peut donc pas revenir au statut Nouveau.

V-D - La protection CSRF

Je vous ai dit plusieurs fois dans ce cours que Laravel met en place systématiquement la protection CSRF. Or, ici, à aucun moment on ne transmet le jeton (token) pour cela !

Si vous regardez dans les headers lors de l'envoi de la requête, vous allez trouver ceci :

X-CSRF-TOKEN:nW57unWQZBJGWaaQ4TyKaVcQsRWYo1UMNbKhxj7u

Le middleware de Laravel qui assure la protection (VerifyCsrfToken) ne se contente pas de chercher le jeton dans les paramètres de la requête : il va aussi voir dans les headers s'il y a une information X-CSRF-TOKEN, ce qui est notre cas.

Mais comment s'est créée cette information ?

Si vous regardez le fichier adminlte/js/back.js, vous allez trouver ce code :

1. $.ajaxSetup({

2. headers: {

3. 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')

4. }

5. })

Le jeton (token) est mémorisé dans les « metas » :

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

Avec la méthode ajaxSetup, on demande à jQuery d'ajouter automatiquement l'information dans les headers. On n'a donc plus à s'en préoccuper ensuite…

V-E - Le traitement côté serveur

L'URL de la requête est de la forme admin/contacts/seen/n, avec n représentant l'identifiant du message.

Voici la partie concernée du contrôleur Back/ContactController :

1. public function updateSeen(Contact $contact)

2. {

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

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

6. }

La requête arrive dans la méthode updateSeen. La liaison implicite permet d'obtenir directement une instance du modèle dans la variable $contact.

On peut alors supprimer l'enregistrement en relation dans la table ingoings :

$contact->ingoing->delete ();

Il suffit ensuite de renvoyer une réponse JSON sans information spécifique :

return response ()->json ();

V-F - Les commentaires

On a déjà vu dans un précédent chapitre qu'on limite le nombre de commentaires affichés pour les articles, et que s'il y en a d'autres, on affiche un bouton :

On a déjà les conditions d'affichage de ce bouton et on va maintenant s'intéresser à son action.

Voici le code du bouton :

<a id="nextcomments" href="{{ route('posts.comments', [$post->id, 1]) }}" class="button">@lang('More comments')</a>

Voici la route concernée :

On a deux paramètres qui sont renseignés avec la méthode route. On se retrouve donc avec une URL dans ce genre :

...posts/2/comments/1

Voici le code JavaScript :

1. $('#nextcomments').click (function(event) {

2. event.preventDefault()

3. $('#morebutton').hide()

4. $('#moreicon').show()

5. $.get($(this).attr('href'))

6. .done(function(data) {

7. $('ol.commentlist').append(data.html)

8. if(data.href !== 'none') {

9. $('#nextcomments').attr('href', data.href)

10. $('#morebutton').show()

11. }

12. $('#moreicon').hide()

13. })

14. })

Sans entrer dans tous les détails, on voit qu'on utilise la méthode $.get de jQuery pour lancer la requête :

$.get($(this).attr('href'))

Au retour on ajoute les commentaires dans la page :

$('ol.commentlist').append(data.html)

S'il faut ajouter un bouton pour les commentaires suivants on le fait :

1. if(data.href !== 'none') {

2. $('#nextcomments').attr('href', data.href)

3. $('#morebutton').show()

4. }

Dans le contrôleur Front/CommentController, on a ce code :

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

On voit qu'on retourne une réponse JSON (c'est automatiquement fait par Laravel) avec deux éléments :

- html : le code HTML des commentaires à ajouter dans la page ;

- href : éventuellement l'URL pour le bouton des commentaires supplémentaires.

V-G - En résumé

- Ajax est facile à mettre en œuvre avec Laravel.

- On peut faire en sorte que la protection CSRF soit automatiquement mise en œuvre pour toutes les requêtes.

VI - Les vues

Je vous ai déjà parlé des vues dans ce cours, ainsi que de Blade, et on a eu plusieurs exemples de code. Dans ce chapitre je vais faire un peu le point et montrer des possibilités intéressantes qui n'ont pas encore été évoquées.

Dans mes précédents cours, je mettais en avant le package Laravel Collective pour générer le HTML. C'est une possibilité élégante et performante mais j'en suis revenu à du codage standard, surtout au vu des améliorations de Blade. C'est juste une question de choix personnel et je vous invite à essayer le package en question pour voir s'il vous convient.

Les exemples de code de ce chapitre sont issus de cette application d'exemple.

Les vues générées par Blade sont en PHP et sont en cache jusqu'à ce qu'elles soient modifiées, ce qui assure de bonnes performances. Le cache se situe ici :

Si vous avez besoin de nettoyer ce cache, ce n'est pas la peine d'aller supprimer ces fichiers dans le dossier : il y a une commande Artisan pour ça :

php artisan view:clear

On en a besoin parfois en cours de développement lorsqu'une vue n'est pas régénérée correctement.

VI-A - L'héritage

On a constaté qu'une vue peut en étendre une autre : c'est un héritage. Ainsi, pour les vues du front, on a un template (layout) de base :

Ce template comporte la structure globale des pages et est déclaré comme parent par les autres vues :

@extends('front.layout')

Dans le template on prévoit trois emplacements (@yield) pour que les vues enfants puissent placer leur code :

1. <!DOCTYPE html>

3. @yield('css')

5. ...

7. @yield('main')

9. ...

11. @yield('scripts')

13. </html>

Le nom de ces emplacements est clair et on peut ainsi préciser des règles CSS, placer le code principal et ajouter du JavaScript.

Ainsi dans la vue front/posts.blade.php, on utilise ces trois emplacements :

1. @extends('front.layout')

3. @section('css')

4. @if (Auth::check())

5. <link rel="stylesheet" href="//cdn.jsdelivr.net/sweetalert2/6.3.8/sweetalert2.min.css">

6. @endif

7. @endsection

9. @section('main')

11. ...

13. @endsection

15. @section('scripts')

17. ...

19. @endsection

On peut également déclarer une section dans le template avec @section lorsqu'il y a du code dedans qu'on veut soit utiliser, soit surcharger.

VI-B - L'inclusion

On peut faire beaucoup de choses avec l'héritage mais il est souvent utile de pouvoir inclure une vue dans une autre. Classiquement, on parle de vue partielle (partial).

VI-B-1 - Les panneaux du tableau de bord

Il y a dans l'application un dossier partials pour le backend :

On y trouve quatre vues partielles. La vue par défaut de l'administration (tableau de bord) est back/index.blade.php :

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

On a peu de code dans cette vue qui étend le layout et renseigne sa section main. La directive @each permet d'itérer dans un tableau ou une collection. Ici, on reçoit du contrôleur la variable $pannels qui contient les renseignements pour les panneaux d'information de l'administration :

Pour chaque panneau, on utilise la vue back/partials/pannel en lui transmettant une variable $pannel :

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>

On a ici la mise en forme du panneau et le renseignement des éléments grâce à la variable.

VI-B-2 - Le menu latéral de l'administration

Le menu latéral de l'administration comporte des icônes, du texte et des sous-menus éventuels :

Dans le layout (back/layout.blade.php), on trouve des appels à la vue partielle back.partials.treeview :

1. @include('back.partials.treeview', [

2. 'icon' => 'user',

3. 'type' => 'user',

4. 'items' => [

5. [

6. 'route' => route('users.index'),

7. 'command' => 'list',

8. 'color' => 'blue',

9. ],

10. [

11. 'route' => route('users.index', ['new' => 'on']),

12. 'command' => 'new',

13. 'color' => 'yellow',

14. ],

15. ],

16. ])

On lui transmet un tableau de valeurs (icône, texte, route, couleur…). Ces informations sont exploitées dans la vue partielle pour créer l'élément de menu :

1. <li class="treeview">

2. <a href="#"><i class="fa fa-fw fa-{{ $icon }}"></i> <span>@lang('admin.menu.' . $type . 's')</span>

3. <span class="pull-right-container">

4. <span class="fa fa-angle-left pull-right"></span>

5. </span>

6. </a>

7. <ul class="treeview-menu">

8. @foreach ($items as $item)

9. <li><a href="{{ $item['route'] }}"><span class="fa fa-fw fa-circle-o text-{{ $item['color'] }}"></span> <span>@lang('admin.menu.' . $item['command'])</span></a></li>

10. @endforeach

11. </ul>

12. </li>

On dispose aussi des directives @includeIf et @includeWhen.

VI-C - Les composants

Comme alternative à l'inclusion, on dispose des composants qui sont apparus plus récemment. Voyons de quoi il s'agit.

Dans les dossiers des vues de l'application d'exemple, vous trouvez pour le backend un dossier components :

Voyons le code de la vue alert :

1. <div class="alert alert-{{ $type }} alert-dismissible">

2. <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>

3. <h4><span class="icon fa fa-check"></span>{{ $slot }}</h4>

4. </div>

On a le code HTML classique d'une barre d'alerte équipée d'un bouton de fermeture. Ce qui est intéressant se situe au niveau de la déclaration des classes et du texte :

class="alert alert-{{ $type }} alert-dismissible"
...
{{ $slot }}

On a deux variables :

- $type : pour le type d'alerte (danger, succès…) ;

- $slot : pour le texte de l'alerte.

Voyons comment on utilise ce composant…

Si vous regardez dans la vue back/users/edit.blade.php, vous trouverez ce code :

1. @if (session('user-updated'))

2. @component('back.components.alert')

3. @slot('type')

4. success

5. @endslot

6. {!! session('user-updated') !!}

7. @endcomponent

8. @endif

On teste si la session contient une variable user-updated et, si c'est le cas, on affiche le composant (@component) back.components.alert en renseignant les deux variables :

- $type : on utilise la directive @slot en précisant le nom de la variable et son contenu ;

- $slot : on aura dans cette variable tout ce qui n'est pas contenu dans une directive @slot, donc dans notre cas le contenu de la variable user-updated en session.

Vous pouvez regarder le code des deux autres composants qui sont basés sur le même principe.

On peut ajouter, si besoin, une information à transmettre au composant avec cette syntaxe :

@component('alert', ['element' => 'info'])

Cette possibilité n'a pas été utilisée dans l'application.

VI-D - Les structures de contrôle

VI-D-1 - @if

La directive @if permet d'introduire une condition. Par exemple, dans la vue front/contact.blade.php, on trouve ce code :

1. @if ($errors->has('name'))

2. @component('front.components.error')

3. {{ $errors->first('name') }}

4. @endcomponent

5. @endif

Cette partie du code gère l'éventuel message d'erreur de validation :

On voit qu'il est fait usage d'un composant pour l'affichage de l'erreur, à savoir (front/components/error.blade.php) :

<span class="red">
<strong>{{ $slot }}</strong>
</span>

un composant tout simple avec juste le slot.

VI-D-2 - @else

La directive @else permet de compléter la logique. Par exemple dans la vue front/pagination.blade.php :

1. @if ($paginator->onFirstPage())

2. <span class="page-numbers prev inactive">@lang('pagination.previous')</span>

3. @else

4. <a href="{{ $paginator->previousPageUrl() }}" class="page-numbers prev" rel="prev">@lang('pagination.previous')</a>

5. @endif

on n’a pas le même affichage selon la page.

VI-D-3 - @unless

La directive @unless est l'inverse de @if. Par exemple, dans la vue front/comments/comments-base.blase.php :

1. @unless ($comment->isLeaf())

2. @php

3. $level++;

4. @endphp

5. <ul class="children">

6. @include('front/comments/comments', ['comments' => $comment->getImmediateDescendants()])

7. </ul>

8. @endunless

on aurait pu écrire :

@if (!$comment->isLeaf())

mais c'est moins élégant…

Remarquez au passage la directive @php qui permet d'insérer du code PHP dans la vue.

VI-D-4 - @isset et @empty

La directive @isset est l'équivalent de la fonction PHP isset(). Dans la vue back/components/box.blade.php, on trouve ce code :

1. @isset($footer)

2. <div class="box-footer">

3. {{ $footer }}

4. </div>

5. @endisset

Si on a un footer, on l'affiche.

Il existe aussi la directive @empty équivalente de la fonction PHP empy() qui n'est pas utilisée dans l'application.

VI-D-5 - @auth et @guest

Il est fréquent d'avoir à tester si un utilisateur est connecté, et on peut écrire :

@if (Auth::check())

mais il est plus élégant d'utiliser @auth. De la même manière, on dispose de @guest.

Il existe aussi la directive @switch équivalente de la directive PHP switch.

VI-D-6 - Condition personnalisée

On peut aussi se construire une condition personnalisée. Regardez ce code dans AppServiceProvider :

1. public function boot()

2. {

3. ...

5. Blade::if('admin', function () {

6. return auth()->user()->role === 'admin';

7. });

9. Blade::if('redac', function () {

10. return auth()->user()->role === 'redac';

11. });

13. Blade::if('request', function ($url) {

14. return request()->is($url);

15. });

16. }

On crée trois directives :

- admin : vérifie qu'un utilisateur est administrateur ;

- redac : vérifie qu'un utilisateur est rédacteur ;

- request : vérifie que la requête correspond à une certaine URL.

Du coup, dans la vue front/layout.blade.php, on peut écrire :

1. @admin

2. <li>

3. <a href="{{ url('admin') }}">@lang('Administration')</a>

4. </li>

5. @endadmin

6. @redac

7. <li>

8. <a href="{{ url('admin/posts') }}">@lang('Administration')</a>

9. </li>

10. @endredac

et dans la même vue :

1. @request('password/reset')

2. <li class="current">

3. <a href="{{ request()->url() }}">@lang('Password')</a>

4. </li>

5. @endrequest

6. @request('password/reset/*')

7. <li class="current">

8. <a href="{{ request()->url() }}">@lang('Password')</a>

9. </li>

10. @endrequest

VI-D-7 - Les boucles

Pour les boucles, on dispose de @for, @foreach, @each, @forelse et @while. La logique est la même que celle des directives équivalentes de PHP. Voyons un exemple dans la vue back/layout.blade.php :

1. @foreach ($breadcrumbs as $item)

2. <li @if ($loop->last && $item['url'] === '#') class="active" @endif>

3. @if ($item['url'] !== '#')

4. <a href="{{ $item['url'] }}">

5. @endif

6. @isset($item['icon'])

7. <span class="fa fa-{{ $item['icon'] }}"></span>

8. @endisset

9. {{ $item['name'] }}

10. @if ($item['url'] !== '#')

11. </a>

12. @endif

13. </li>

14. @endforeach

C'est la partie du code qui gère le breadcrumb :

Avec @foreach, on itère dans les éléments de la variable @breadcrumbs. Avec cette directive, on dispose de la variable interne $loop qui offre des informations sur l'index en cours. Voici tout ce qui est disponible :

Propriété

Description

$loop->index

L'index en cours (commence à 0)

$loop->iteration

L'itération courante (commence à 1)

$loop->remaining

Les itérations restantes

$loop->count

Le nombre total d'itérations

$loop->first

Indique si c'est le premier élément

$loop->last

Indique si c'est le dernier élément

$loop->depth

Le niveau d'imbrication actuel

$loop->parent

La variable d'itération parente si imbrication

Dans notre code on vérifie si on a le dernier élément avec $loop->last. On utilise alors quelques directives qu'on a déjà vues.

VI-E - L'injection de service

Normalement, on envoie à une vue toutes les informations nécessaires à partir du contrôleur, mais parfois on a besoin d'un service spécifique : on peut alors utiliser la directive @inject. Vous pouvez trouver un exemple dans la vue back/settings.blade.php :

@inject('envRepository', 'App\Repositories\EnvRepository')

Le service est fourni par la classe App\Repositories\EnvRepository (c'est un repository pour interagir avec le fichier .env) dont une instance va se trouver dans la variable $envRepository. On peut ainsi écrire ensuite dans la vue ce code :

$envRepository->get('APP_URL')

Ici, on peut récupérer la valeur pour l'élément APP_URL du fichier .env. Elle est ainsi affichée dans les réglages :

VI-F - Les composeurs de vue (view composers)

Les « composeurs de vue » sont des fonctions de rappel ou des classes qui sont appelées lorsqu'une vue est générée, ce qui permet de localiser du code. Voyons un exemple dans l'application.

Regardez ce code dans AppServiceProvider (on aurait pu créer un provider spécifique) :

1. public function boot()

2. {

3. ...

5. view()->composer('front.layout',MenuComposer::class);

7. view()->composer('back.layout',HeaderComposer::class);

9. ...

10. }

Avec la première ligne, on dit que lorsque la vue front/layout.blade.php est générée, on doit appeler la classe App\Http\ViewComposers\MenuComposer.

Les classes pour les composeurs de vue sont toutes rangées dans ce dossier :

Voyons cette classe :

1. <?php

3. namespace App\Http\ViewComposers;

5. use Illuminate\View\View;

6. use App\Models\Category;

8. class MenuComposer

9. {

10. /**

11. * Bind data to the view.

12. *

13. * @param  View  $view

14. * @return void

15. */

16. public function compose(View $view)

17. {

18. $view->with('categories', Category::select('title', 'slug')->get());

19. }

20. }

On constate qu'on envoie (with) dans la vue une variable $categories avec les titres des catégories. C'est pour renseigner ce sous-menu :

On a constaté que la vue front/layout.blade.php est le template de base pour toutes les vues du frontend. Si on ne passait pas par l'intermédiaire d'un composeur de vue, il faudrait envoyer les catégories dans toutes les vues ! Ici on a le code en un seul endroit.

C'est la même logique pour la classe HeaderComposer en ce qui concerne le template du backend : elle est juste un peu plus chargée parce qu'on doit gérer le breadcrumb, les titres et les notifications.

VI-G - En résumé

Avec Blade on peut :

- utiliser l'héritage de vue ;

- inclure une vue ;

- utiliser un composant ;

- utiliser de nombreuses directives pour organiser le code ;

- utiliser des boucles ;

- injecter un service.

D'autre part, les composeurs de vue permettent de localiser du code facilement.

VII - Les tests

Les développeurs PHP n'ont pas été habitués à faire des tests pour leurs applications. Cela est dû à l'histoire de ce langage qui n'était au départ qu'une possibilité de scripter au milieu du code HTML, mais qui s'est peu à peu développé comme un langage de plus en plus évolué. Les créateurs de frameworks ont initié une autre façon d'organiser le code de PHP : en particulier ils ont mis en avant la séparation des tâches qui a rendu la création de tests possible.‌

Laravel a été pensé pour intégrer des tests. Il comporte une infrastructure élémentaire et des helpers. Nous allons voir dans ce chapitre cet aspect de Laravel. Considérez ce que je vais vous dire ici comme une simple introduction à ce domaine qui mériterait à lui seul un cours spécifique. Je vais m'efforcer de vous démontrer l'utilité de créer des tests, comment les préparer et comment les isoler.

Lorsqu'on développe avec PHP, on effectue forcément des tests, au moins manuels. Par exemple, si vous créez un formulaire, vous allez l'utiliser, entrer diverses informations, essayer des fausses manœuvres… Imaginez que tout cela soit automatisé et que vous n'ayez qu'à cliquer pour lancer tous les tests. C'est le propos de ce chapitre.

Vous pouvez aussi vous dire qu'écrire des tests conduit à du travail supplémentaire, que ce n'est pas toujours facile, que ce n'est pas nécessaire dans tous les cas. C'est à vous de voir si vous avez besoin d'en créer ou pas. Pour des petites applications la question reste ouverte. Par contre, dès qu'une application prend de l'ampleur ou lorsqu'elle est conduite par plusieurs personnes, alors il devient vite nécessaire de créer des tests automatisés.

VII-A - L'intendance des tests

VII-A-1 - PHPUnit

Laravel utilise PHPUnit pour effectuer les tests. C'est un framework créé par « Sebastian Bergmann » qui fonctionne à partir d'assertions.

Ce framework est installé comme dépendance de Laravel en mode développement :

1. "require-dev": {

2. "filp/whoops": "~2.0",

3. "fzaninotto/faker": "~1.4",

4. "mockery/mockery": "0.9.*",

5. "phpunit/phpunit": "~6.0"

6. },

Mais vous pouvez aussi utiliser le fichier phar, que vous pouvez trouver sur cette page, et le placer à la racine de votre application. Vous êtes alors prêt à tester !

La version 6 de PHPUnit nécessite au minimum PHP 7

Vous pouvez vérifier que ça fonctionne en entrant cette commande :

php phpunit(numéro de version).phar -h

Vous obtenez ainsi la liste de toutes les commandes disponibles.

Si vous utilisez la version installée avec Laravel ça donne :

php vendor\phpunit\phpunit\phpunit -h

Je vous conseille de vous faire un alias !

Il y a aussi la possibilité d'utiliser les fichiers placés dans le dossier vendor/bin.

Dans tous les exemples de ce chapitre j'utiliserai le fichier phar en réduisant son nom à phpunit.

VII-B - L'intendance de Laravel

Si vous regardez les dossiers de Laravel vous allez en trouver un qui est consacré aux tests :

Vous avez déjà deux dossiers et deux fichiers. Voilà le code de TestCase.php :

1. <?php

3. namespace Tests;

5. use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

7. abstract class TestCase extends BaseTestCase

8. {

9. use CreatesApplication;

10. }

On voit qu'on utilise le trait CreatesApplication.

Cette classe est chargée de créer une application pour les tests dans un environnement spécifique (ce qui permet de mettre en place une configuration adaptée aux tests).

Elle étend la classe Illuminate\Foundation\Testing\TestCase, qui elle-même étend la classe PHPUnit\Framework\TestCase, en lui ajoutant quelques fonctionnalités bien pratiques, comme nous allons le voir.

Toutes les classes de test que vous allez créer devront étendre cette classe TestCase.

On a deux exemples de tests déjà présents, un dans Unit\ExampleTest.php :

1. <?php

3. namespace Tests\Unit;

5. use Tests\TestCase;

6. use Illuminate\Foundation\Testing\RefreshDatabase;

8. class ExampleTest extends TestCase

9. {

10. /**

11. * A basic test example.

12. *

13. * @return void

14. */

15. public function testBasicTest()

16. {

17. $this->assertTrue(true);

18. }

19. }

et un autre dans Features\ExampleTest.php :

1. <?php

3. namespace Tests\Feature;

5. use Tests\TestCase;

6. use Illuminate\Foundation\Testing\RefreshDatabase;

8. class ExampleTest extends TestCase

9. {

10. /**

11. * A basic test example.

12. *

13. * @return void

14. */

15. public function testBasicTest()

16. {

17. $response = $this->get('/');

19. $response->assertStatus(200);

20. }

21. }

Pourquoi deux dossiers ?

Si on lit la documentation sur le sujet on trouve cette explication :

By default, your application's tests directory contains two directories: Feature and Unit. Unit tests are tests that focus on a very small, isolated portion of your code. In fact, most unit tests probably focus on a single method. Feature tests may test a larger portion of your code, including how several objects interact with each other or even a full HTTP request to a JSON endpoint.

En gros, dans Feature, on va mettre des tests plus généraux, pas vraiment unitaires pour le coup. Mais vous pouvez utiliser ces deux dossiers « à votre convenance », et donc n'en utiliser qu'un seul.

Sans entrer pour le moment dans le code, sachez simplement que dans le premier exemple, on se contente de demander si un truc vrai est effectivement vrai (bon c'est sûr que ça devrait être vrai ^^). Dans le second, on envoie une requête pour la route de base et on attend une réponse positive (200).

Pour lancer ces tests, c'est très simple : entrez cette commande :

On voit qu'ont été effectués deux tests et deux assertions, et que tout s'est bien passé.

VII-B-1 - L'environnement de test

Je vous ai dit que les tests s'effectuent dans un environnement particulier, ce qui est bien pratique.

Où se trouve cette configuration ?

Regardez le fichier phpunit.xml :

1. <?xml version="1.0" encoding="UTF-8"?>

2. <phpunit backupGlobals="false"

3. backupStaticAttributes="false"

4. bootstrap="vendor/autoload.php"

5. colors="true"

6. convertErrorsToExceptions="true"

7. convertNoticesToExceptions="true"

8. convertWarningsToExceptions="true"

9. processIsolation="false"

10. stopOnFailure="false">

11. <testsuites>

12. <testsuite name="Feature">

13. <directory suffix="Test.php">./tests/Feature</directory>

14. </testsuite>

16. <testsuite name="Unit">

17. <directory suffix="Test.php">./tests/Unit</directory>

18. </testsuite>

19. </testsuites>

20. <filter>

21. <whitelist processUncoveredFilesFromWhitelist="true">

22. <directory suffix=".php">./app</directory>

23. </whitelist>

24. </filter>

25. <php>

26. <env name="APP_ENV" value="testing"/>

27. <env name="CACHE_DRIVER" value="array"/>

28. <env name="SESSION_DRIVER" value="array"/>

29. <env name="QUEUE_DRIVER" value="sync"/>

30. </php>

31. </phpunit>

On trouve déjà quatre variables d'environnement :

- APP_ENV : là, on dit qu'on est en mode testing ;

- CACHE_DRIVER : en mode array, ce qui signifie qu'on ne va rien mettre en cache pendant les tests (par défaut on a file) ;

- SESSION_DRIVER : en mode array, ce qui signifie qu'on ne va pas faire persister la session (par défaut on a file) ;

- QUEUE_DRIVER : en mode sync, donc on aura pas de file d'attente.

On peut évidemment ajouter les variables dont on a besoin. Par exemple si pendant les tests je ne veux plus MySql mais sqlite : il y a une variable dans le fichier .env :

DB_CONNECTION=mysql

que je peux modifier. Pour ce faire, dans phpunit.xml, je peux écrire :

<env name="DB_CONNECTION" value="sqlite"/>

Maintenant, pour les tests, je vais utiliser sqlite.

VII-C - Construire un test

VII-C-1 - Les trois étapes d'un test

Pour construire un test, on procède généralement en trois étapes :

1. On initialise les données ;

2. On agit sur ces données ;

3. On vérifie que le résultat est conforme à notre attente.

Comme tout ça est un peu abstrait, prenons un exemple. Remplacez le code de la méthode testBasicTest (peu importe dans quel dossier) par celui-ci :

1. public function testBasicTest()

2. {

3. $data = [10, 20, 30];

4. $result = array_sum($data);

5. $this->assertEquals(60, $result);

6. }

Supprimez le test dans l'autre dossier pour éviter de polluer les résultats.

On trouve nos trois étapes. On initialise les données :

$data = [10, 20, 30];

On agit sur ces données :

$result = array_sum($data);

On teste le résultat :

$this->assertEquals(60, $result);

La méthode assertEquals permet de comparer deux valeurs, ici 60 et $result. Si vous lancez le test vous obtenez :

Vous voyez à nouveau l'exécution d'un test et d'une assertion. Le tout s'est bien passé. Changez la valeur 60 par une autre et vous obtiendrez ceci :

Vous connaissez maintenant le principe de base d'un test et ce qu'on peut obtenir comme renseignement en cas d'échec.

VII-D - Assertions et appel de route

VII-D-1 - Les assertions

Les assertions constituent l'outil de base des tests.

Voici quelques assertions et l'utilisation d'un helper de Laravel que l'on teste au passage :

1. public function testBasicTest()

2. {

3. $data = 'Je suis petit';

4. $this->assertTrue(starts_with($data, 'Je'));

5. $this->assertFalse(starts_with($data, 'Tu'));

6. $this->assertSame(starts_with($data, 'Tu'), false);

7. $this->assertStringStartsWith('Je', $data);

8. $this->assertStringEndsWith('petit', $data);

9. }

Lorsqu'on lance le test, on obtient ici :

un test et cinq assertions correctes.

VII-D-2 - Appel de route et test de réponse

Il est facile d'appeler une route pour effectuer un test sur la réponse. Modifiez la route de base pour celle-ci :

Route::get('/', function () {
return 'coucou';
});

On a donc une requête avec l'URL de base et comme réponse la chaîne coucou. Nous allons vérifier que la requête aboutit bien, qu'il y a une réponse correcte, et que la réponse est coucou :

1. public function testBasicTest()

2. {

3. $response = $this->call('GET', '/');

4. $response->assertSuccessful();

5. $this->assertEquals('coucou', $response->getContent());

6. }

L'assertion assertSuccessful nous assure que la réponse est correcte. Ce n'est pas une assertion de PHPUnit : elle est spécifique à Laravel. Vous trouverez toutes les assertions de Laravel ici.

La méthode getContent permet de lire la réponse. On obtient :

VII-E - Les vues et les contrôleurs

VII-E-1 - Les vues

Qu'en est-il si on retourne une vue ?

Mettez ce code pour la route :

Route::get('/', function () {
return view('welcome')->with('message', 'Vous y êtes !');
});

Ajoutez dans cette vue ceci :

{{ $message }}

Maintenant voici le test :

1. public function testBasicTest()

2. {

3. $response = $this->call('GET', '/');

4. $response->assertViewHas('message', 'Vous y êtes !');

5. }

On envoie la requête et on récupère la réponse. On peut tester la valeur de la variable $message dans la vue avec l'assertion assertViewHas :

VII-E-2 - Les contrôleurs

Créez ce contrôleur :

1. <?php

3. namespace App\Http\Controllers;

5. use Illuminate\Http\Request;

7. class WelcomeController extends Controller

8. {

9. public function index()

10. {

11. return view('welcome');

12. }

13. }

Créez cette route pour mettre en œuvre le contrôleur ci-dessus :

Route::get('welcome', 'WelcomeController@index');

Vérifiez que ça fonctionne (vous aurez peut-être besoin de retoucher la vue où nous avons introduit une variable).

Supprimez le fichier ExampleTest.php qui ne va plus nous servir.

Créez ce test avec Artisan :

php artisan make:test WelcomeControllerTest

Par défaut vous obtenez ce code :

1. <?php

3. namespace Tests\Feature;

5. use Tests\TestCase;

6. use Illuminate\Foundation\Testing\RefreshDatabase;

8. class WelcomeControllerTest extends TestCase

9. {

10. /**

11. * A basic test example.

12. *

13. * @return void

14. */

15. public function testExample()

16. {

17. $this->assertTrue(true);

18. }

19. }

Changez ainsi le code de la méthode :

1. public function testIndex()

2. {

3. $response = $this->call('GET', 'welcome');

4. $response->assertStatus(200);

5. }

On n'a plus de méthode spécifique pour les contrôleurs comme c'était le cas avant.

VII-F - Isoler les tests

Nous allons maintenant aborder un aspect important des tests qui ne s'appellent pas unitaires pour rien.

Pour faire des tests efficaces, il faut bien les isoler, donc savoir ce qu'on teste, ne tester qu'une chose à la fois et ne pas mélanger les choses.

Ceci est possible si le code est bien organisé, ce que je me suis efforcé de vous montrer depuis le début de ce cours.

Avec PHPUnit, chaque test est effectué dans une application spécifique : il n'est donc pas possible de les rendre dépendants les uns des autres.

En général on utilise Mockery, un composant qui permet de simuler le comportement d'une classe. Il est déjà prévu dans l'installation de Laravel en mode développement :

1. "require-dev": {

2. "filp/whoops": "~2.0",

3. "fzaninotto/faker": "~1.4",

4. "mockery/mockery": "0.9.*",

5. "phpunit/phpunit": "~6.0"

6. },

Le fait de prévoir ce composant uniquement pour le développement simplifie ensuite la mise en œuvre pour le déploiement. Normalement, vous devriez trouver ce composant dans vos dossiers :

VII-F-1 - Simuler une classe

Nous allons voir maintenant comment l'utiliser, mais pour cela on va mettre en place le code à tester. Ce ne sera pas trop réaliste, mais c'est juste pour comprendre le mécanisme de fonctionnement de Mockery. Remplacez le code du contrôleur WelcomeController par celui-ci :

1. <?php

3. namespace App\Http\Controllers;

5. use App\Services\Livre;

7. class WelcomeController extends Controller

8. {

9. public function __construct()

10. {

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

12. }

14. public function index(Livre $livre)

15. {

16. $titre = $livre->getTitle();

18. return view('welcome', compact('titre'));

19. }

20. }

J'ai prévu l'injection d'une classe dans la méthode index. Voilà la classe en question :

1. <?php

3. namespace App\Services;

5. class Livre

6. {

7. public function getTitle() {

8. return 'Titre';

9. }

10. }

Bon, d'accord, ce n'est pas très joli, mais c'est juste pour la démonstration.

La difficulté ici réside dans la présence de l'injection d'une classe. Comme on veut isoler les tests, l'idéal serait de pouvoir simuler cette classe. C'est justement ce que permet de faire Mockery.

Voici la classe de test que nous allons utiliser :

1. <?php

3. namespace Tests\Feature;

5. use Tests\TestCase;

6. use Illuminate\Foundation\Testing\RefreshDatabase;

7. use Illuminate\Support\Collection;

8. use App\Services\Livre;

9. use Mockery;

11. class WelcomeControllerTest extends TestCase

12. {

13. public function testIndex()

14. {

15. // Création Mock

16. $mock = Mockery::mock(Livre::class)

17. ->shouldReceive('getTitle')

18. ->andReturn('Titre');

20. // Création lien

21. $this->app->instance('\App\Services\Livre', $mock);

23. // Action

24. $response = $this->call('GET', 'welcome');

26. // Assertions

27. $response->assertSuccessful();

28. $response->assertViewHas('titre', 'Titre');

30. }

32. public function tearDown()

33. {

34. Mockery::close();

35. }

36. }

Et voici le code à ajouter dans la vue pour faire réaliste :

{{ $titre }}

Si je lance le test, j'obtiens :

Voyons de plus près ce code… On crée un objet Mock en lui demandant de simuler la classe Livre :

$mock = Mockery::mock(Livre::class)

Ensuite on définit le comportement que l'on désire pour cet objet :

->shouldReceive('getTitle')
->andReturn('Titre');

On lui dit qu'il reçoit (shouldReceive) l'appel de la méthode getTitle et doit retourner Titre.

Ensuite on informe le conteneur de Laravel de la liaison entre la classe Livre et notre objet Mock :

$this->app->instance('\App\Services\Livre', $mock);

C'est une façon de dire à Laravel : chaque fois que tu auras besoin de la classe Livre tu iras plutôt utiliser l'objet $mock.

Ensuite on effectue l'action, ici la requête :

$response = $this->call('GET', 'welcome');

Pour finir, on prévoit deux assertions : une pour vérifier qu'on a une réponse correcte et la seconde pour vérifier qu'on a bien le titre dans la vue :

$response->assertSuccessful();
$response->assertViewHas('titre', 'Titre');

Vous connaissez maintenant le principe de l'utilisation de Mockery. Il existe de vastes possibilités avec ce composant.

Il n'y a pas vraiment de règle quant à la constitution des tests, ni pour ce qu'il faut tester ou pas. L'important est de comprendre comment les faire et de juger ce qui est utile ou pas selon les circonstances. Une façon efficace d'apprendre à réaliser des tests, tout en comprenant mieux Laravel, est de regarder comment ces tests ont été conçus.

VII-G - Tester une application (dusk)

Laravel va encore plus loin dans la convivialité pour les tests en offrant la possibilité de tester facilement une application. « Laravel Dusk » utilise par défaut ChromeDriver, mais c'est totalement transparent.

VII-G-1 - Installation

Il faut commencer par l'installer :

composer require --dev laravel/dusk

et ensuite utiliser cette commande :

php artisan dusk:install

Vous allez vous retrouver avec un nouveau dossier dans les tests :

Enfin il faut renseigner correctement la variable APP_URL dans le fichier .env :

APP_URL=http://monsite.dev

Pour lancer les tests, c'est :

php artisan dusk

mais on peut aussi se limiter à un groupe de tests :

php artisan dusk --group=groupe1

VII-G-2 - Exemples

On va voir ça en œuvre avec l'application d'exemple. Voici l'ensemble des fichiers de test :

Ils sont classés par catégories.

Pour que ça fonctionne il faut régénérer la base de données avec les valeurs par défaut :

php artisan migrate:fresh --seed

Il faut aussi régler la locale sur en :

'locale' => 'en',

On va voir les tests du groupe login (LoginTest.php) :

1. <?php

3. namespace Tests\Browser;

5. use Tests\DuskTestCase;

6. use Laravel\Dusk\Browser;

8. class LoginTest extends DuskTestCase

9. {

10. /**

11. * Test login by name and logout

12. * @group login

13. *

14. * @return void

15. */

16. public function testLoginByNameAndLogout()

17. {

18. $this->browse(function (Browser $browser) {

19. $browser->visit('/login')

20. ->type('log', 'Slacker')

21. ->type('password', 'slacker')

22. ->press('Login')

23. ->assertPathIs('/')

24. ->assertSee('Logout')

25. ->clickLink('Logout')

26. ->assertSee('Login');

27. });

28. }

30. /**

31. * Test login by email

32. * @group login

33. *

34. * @return void

35. */

36. public function testLoginByEmail()

37. {

38. $this->browse(function (Browser $browser) {

39. $browser->visit('/login')

40. ->type('log', 'redac@la.fr')

41. ->type('password', 'redac')

42. ->press('Login')

43. ->assertPathIs('/')

44. ->assertSee('Logout')

45. ->clickLink('Logout');

46. });

47. }

49. /**

50. * Test login fail

51. * @group login

52. *

53. * @return void

54. */

55. public function testLoginFail()

56. {

57. $this->browse(function (Browser $browser) {

58. $browser->visit('/login')

59. ->type('log', 'toto@la.fr')

60. ->type('password', 'toto')

61. ->press('Login')

62. ->assertSee('These credentials do not match our records');

63. });

64. }

65. }

On les lance ainsi :

php artisan dusk --group=login

et si tout va bien :

Voyons un peu le code, par exemple testLoginByNameAndLogout. On commence par visiter la page de login :

$browser->visit('/login')

On entre (type) la valeur Slacker dans le champ log :

->type('log', 'Slacker')

On entre (type) la valeur slacker dans le champ password :

->type('password', 'slacker')

On actionne (press) le bouton Login :

->press('Login')

On vérifie que l'URL (assertPathIs) est « / » :

->assertPathIs('/')

On vérifie qu'il y a dans la page (assertSee) Logout :

->assertSee('Logout')

On clique sur le lien (clickLink) Logout :

->clickLink('Logout')

On vérifie qu'il y a dans la page (assertSee) Login :

->assertSee('Login');

Vous voyez que c'est d'une grande simplicité ! La documentation complète est ici.

Vous pouvez explorer le code des autres fichiers et lancer les tests. Récemment il y a eu quelques bugs après en avoir lancé plusieurs. Il vaut donc mieux se limiter à un groupe.

VII-H - En résumé

- Laravel utilise PHPUnit pour effectuer les tests unitaires.

- En plus des méthodes de PHPUnit, on dispose d'helpers pour intégrer les tests dans une application réalisée avec Laravel.

- Le composant Mockery permet de simuler le comportement d'une classe et donc de bien isoler les tests.

- Laravel propose des commandes conviviales pour tester une application avec dusk.

VIII - Les fichiers et le cache

On a vu comment utiliser une base de données pour gérer des informations, mais ce n'est pas la seule façon de les mémoriser et de les retrouver. De plus, il y a bien d'autres éléments à gérer, en particulier des fichiers. Dans ce chapitre, on va voir comment Laravel permet avec élégance de gérer des fichiers, ainsi que le système de cache qui peut booster vos applications.

VIII-A - Le système de gestion des fichiers

Laravel fournit un système de gestion des fichiers suffisamment abstrait pour s'adapter à toutes les situations, que ce soit en local ou sur un nuage (cloud). Sous le capot, c'est le package flysystem qui est utilisé. Les avantages de ce package peuvent se résumer ainsi :

- on dispose d'une API unique pour tous les systèmes de gestion (local ou nuage) ;

- on peut utiliser un cache ;

- permet de gérer de gros fichiers (stream) ;

- facile à tester.

Par défaut on dispose de nombreux drivers, en particulier :

- local ;

- Azure ;

- AWS S3 ;

- Dropbox ;

- FTP ;

- Rackspace…

VIII-A-1 - La configuration

La configuration se trouve dans le fichier config/filesystem.php avec ce code par défaut :

1. 'disks' => [

3. 'local' => [

4. 'driver' => 'local',

5. 'root' => storage_path('app'),

6. ],

8. 'public' => [

9. 'driver' => 'local',

10. 'root' => storage_path('app/public'),

11. 'url' => env('APP_URL').'/storage',

12. 'visibility' => 'public',

13. ],

15. 's3' => [

16. 'driver' => 's3',

17. 'key' => env('AWS_KEY'),

18. 'secret' => env('AWS_SECRET'),

19. 'region' => env('AWS_REGION'),

20. 'bucket' => env('AWS_BUCKET'),

21. ],

23. ],

On raisonne en « disques » (disks), chacun utilisant un driver et un emplacement :

- local : interagit avec les fichiers locaux et présume qu'ils se trouvent dans le dossier storage/app ;

- public : interagit avec les fichiers locaux qui doivent être accessibles par les clients. Le dossier par défaut est storage/app/public, mais ce dossier n'est pas accessible par défaut. Il faut alors ajouter un lien symbolique (symlink) pour pointer quelque part dans le dossier public. On dispose d'une commande d'Artisan pour le faire :

php artisan storage:link

La motivation principale est d'avoir les fichiers accessibles publiquement dans un dossier géré par les outils de déploiement comme Envoyer. A vous de voir si vous voulez suivre cette procédure ou tout simplement diriger tout sur le dossier public.

- s3 : c'est un exemple de configuration d'un nuage avec renseignement des paramètres. Pour que ça fonctionne, il faut installer le package correspondant. Par exemple pour s3, c'est league/flysystem-aws-s3-v3 ~1.0.

Le disque par défaut est précisé dans le même fichier de configuration :

'default' => env('FILESYSTEM_DRIVER', 'local'),

VIII-A-2 - La façade

Une fois que vous avez défini vos disques dans la configuration, vous pouvez utiliser la façade Storage pour gérer les fichiers. Il suffit de préciser le disque concerné par la manipulation (s'il n'est pas précisé, ça sera le disque par défaut défini dans la configuration). Par exemple, avec :

Storage::disk('s3')->get('image.png');

on va aller chercher dans le nuage s3 l'image image.png.

On dispose de nombreuses méthodes pour manipuler les fichiers, voici les principales :

- get : récupération d'un fichier comme on l'a vu ci-dessus ;

- put : sauvegarde d'un contenu dans un fichier put('fichier.txt', $contenu) ;

- putFile : sauvegarde le contenu d'un fichier dans un emplacement putFile('dossier', $fichier) ;

- exists : détermine si un fichier existe exists('dossier', $fichier) ;

- delete : supprime un fichier delete('fichier.txt') ;

- deleteDirectory : supprime un dossier deleteDirectory('dossier') ;

- copy : copie un fichier copy('fichier.txt', 'nouveaufichier.txt') ;

- move : déplace un fichier move('fichier.txt', 'nouveaufichier.txt') ;

- files : retourne un tableau avec les noms des fichiers files('dossier') ;

- directories : retourne un tableau avec les noms des dossiers directories('dossier') ;

- prepend : ajoute un contenu au début d'un fichier prepend('fichier.txt', 'texte') ;

- append : ajoute un contenu à la fin d'un fichier append('fichier.txt', 'texte').

VIII-B - Un exemple

Dans l'application d'exemple, on a cette configuration :

1. 'disks' => [

3. 'local' => [

4. 'driver' => 'local',

5. 'root' => storage_path('app'),

6. ],

8. 'public' => [

9. 'driver' => 'local',

10. 'root' => storage_path('app/public'),

11. 'url' => env('APP_URL').'/storage',

12. 'visibility' => 'public',

13. ],

15. 'files' => [

16. 'driver' => 'local',

17. 'root' => public_path() . ('/files'),

18. 'visibility' => 'public',

19. ],

21. 'thumbs' => [

22. 'driver' => 'local',

23. 'root' => public_path() . ('/thumbs'),

24. 'visibility' => 'public',

25. ],

27. ...

29. ],

On a les valeurs par défaut pour local et public, et on a deux disques supplémentaires :

- files : pour les médias ;

- thumbs : pour les petites images des articles dans l'administration.

On voit que les deux sont placés dans public :

VIII-B-1 - Les médias

Dans l'application, les médias sont gérés par elFinder :

par l'intermédiaire de ce package pour l'adaptation à Laravel.

La configuration du package se situe dans ce fichier :

J'ai juste ajouté ces middlewares parce qu'il faut être au moins rédacteur pour accéder à la gestion des médias :

1. 'route' => [

2. 'prefix' => 'elfinder',

3. 'middleware' => ['web', 'redac'],

4. ],

Un administrateur a accès à tous les médias mais un rédacteur doit être limité à ses propres médias pour éviter qu'ils aillent perturber les autres.

Si vous regardez le contenu du dossier public/files, vous trouverez ceci :

Chaque rédacteur a un dossier de la forme user{id}. Par défaut, on a un seul rédacteur avec l'identifiant « 2 », et donc son dossier est user2 :

Dans le modèle app\Models\User, on trouve cette méthode :

1. /**

2. * Get user files directory

3. *

4. * @return string|null

5. */

6. public function getFilesDirectory()

7. {

8. if ($this->role === 'redac') {

9. $folderPath = 'user' . $this->id;

10. if (!in_array($folderPath , Storage::disk('files')->directories())) {

11. Storage::disk('files')->makeDirectory($folderPath);

12. }

13. return $folderPath;

14. }

15. return null;

16. }

Elle retourne le nom du dossier du rédacteur et au besoin on le crée s'il n'existe pas. On voit l'utilisation de la méthode directories de Storage sur le disque files pour trouver tous les dossiers :

Storage::disk('files')->directories()

Si le dossier n'existe pas, alors on le crée avec makeDirectory :

Storage::disk('files')->makeDirectory($folderPath);

VIII-B-2 - Les thumbs

Lorsqu'on liste les articles dans l'administration, on visualise une miniature de l'image d'illustration de l'article :

Il serait lourd de charger les images en haute résolution, même s'il ne s'agit que de l'administration, alors l'application génère des miniatures dans le dossier thumbs :

J'ai déjà évoqué ce sujet dans le chapitre sur les événements. J'ai alors dit que c'est le service app\Services\Thumb qui est chargé de la génération des miniatures lorsqu'on crée ou modifie un article :

Voici la principale méthode de ce service :

1. public static function makeThumb(Model $model)

2. {

3. if ($model instanceof Post) {

4. $path = $model->image;

5. $dir = dirname ($path);

6. if ($dir != '\files') {

7. $dir = substr_replace ($dir, '', 0, 7);

8. if (!in_array($dir , Storage::disk('thumbs')->directories())) {

9. Storage::disk('thumbs')->makeDirectory($dir);

10. }

11. }

12. $image = Image::make(url($model->image))->widen(100);

13. Storage::disk('thumbs')->put(substr_replace (self::makeThumbPath($path), '', 0, 7), $image->encode());

14. }

15. }

On retrouve la routine de création du dossier telle qu'on l'a vue pour les médias puisque la structure de thumbs doit être la même que celle de files :

if (!in_array($dir , Storage::disk('thumbs')->directories())) {
Storage::disk('thumbs')->makeDirectory($dir);
}

On crée la miniature avec le superbe package intervention/image :

$image = Image::make(url($model->image))->widen(100);

Ensuite on utilise la méthode put de Storage pour mémoriser la miniature sur le disque thumbs :

Storage::disk('thumbs')->put(substr_replace (self::makeThumbPath($path), '', 0, 7), $image->encode());

VIII-C - Le cache

Laravel fournit une API unique pour tous les drivers de cache gérés comme Memcached ou Redis. Ça fonctionne un peu comme les sessions avec un système clé-valeur. Par défaut le cache utilise un fichier.

VIII-C-1 - La configuration

Le fichier de configuration est ici :

On voit que par défaut on utilise un fichier (file) :

'default' => env('CACHE_DRIVER', 'file'),

Ensuite vous avez tous les systèmes disponibles :

1. 'stores' => [

3. 'apc' => [

4. 'driver' => 'apc',

5. ],

7. 'array' => [

8. 'driver' => 'array',

9. ],

11. 'database' => [

12. 'driver' => 'database',

13. 'table' => 'cache',

14. 'connection' => null,

15. ],

17. 'file' => [

18. 'driver' => 'file',

19. 'path' => storage_path('framework/cache/data'),

20. ],

22. 'memcached' => [

23. 'driver' => 'memcached',

24. 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),

25. 'sasl' => [

26. env('MEMCACHED_USERNAME'),

27. env('MEMCACHED_PASSWORD'),

28. ],

29. 'options' => [

30. // Memcached::OPT_CONNECT_TIMEOUT  => 2000,

31. ],

32. 'servers' => [

33. [

34. 'host' => env('MEMCACHED_HOST', '127.0.0.1'),

35. 'port' => env('MEMCACHED_PORT', 11211),

36. 'weight' => 100,

37. ],

38. ],

39. ],

41. 'redis' => [

42. 'driver' => 'redis',

43. 'connection' => 'default',

44. ],

46. ],

Donc, par défaut, ça aboutit ici :

VIII-C-2 - Utilisation

Vous disposez de la façade Cache et de l'helper cache() pour utiliser le cache.

VIII-C-2-a - Mettre en cache

Pour mettre en cache, on utilise la méthode put :

Cache::put('clé', 'valeur', $minutes);
// ou
cache(['clé' => 'valeur'], $minutes);

Pour vérifier que la clé n'existe pas déjà, et risquer de l'écraser, utilisez add au lieu de put.

Il faut obligatoirement indiquer une durée en minutes. Si vous voulez une durée infinie, alors utilisez la méthode forever :

Cache::forever('clé', 'valeur');
ou cache()->forever('clé', 'valeur');

VIII-C-2-b - Récupérer une valeur en cache

Pour récupérer une valeur en cache, on utilise get :

$valeur = Cache::get('clé');
// ou
$valeur = cache('clé');

Si la clé n'existe pas, la méthode retourne null, ce qui peut être parfois gênant. Il est possible d'indiquer une valeur par défaut :

$valeur = Cache::get('clé', 'défaut');
// ou
$valeur = cache('clé', 'défaut');

Mais vous pouvez avoir envie, si la clé n'existe pas, de mémoriser une valeur. Voici la syntaxe à utiliser :

1. $value = Cache::remember('clé', $minutes, function () {

2. return genereCle();

3. });

4. // ou

5. $value = cache()->remember('clé', $minutes, function () {

6. return genereCle();

7. });

Et si vous avez envie de récupérer la valeur et de la supprimer en même temps, alors utilisez « pull ».

VIII-C-2-c - Supprimer une valeur du cache

Pour supprimer une valeur du cache on utilise forget :

Cache::forget('clé');
// ou
cache()->forget('clé');

et si vous voulez carrément vider tout le cache, utilisez flush :

Cache::flush();
// ou
cache()->flush();

VIII-C-2-d - Vérifier qu'une clé existe

Enfin, on peut vérifier l'existence d'une clé avec has :

1. if (Cache::has('clé')) {

2. //

3. }

4. // ou

5. if (cache()->has('clé')) {

6. //

7. }

VIII-C-3 - Un exemple

Vous pouvez trouver un exemple expliqué avec une démonstration en ligne ici. L'article date un peu mais reste pertinent.

Il y a encore pas mal de possibilités avec ce cache, et vous pouvez trouver tout ça dans la documentation.

VIII-C-4 - Les routes et la configuration

VIII-C-4-a - Les routes

À chaque requête, Laravel doit parcourir le fichier des routes pour trouver la bonne. Cette action peut être accélérée si on crée un cache des routes :

php artisan route:cache

Le fichier est créé ici :

Mais attention ! Il ne faut pas qu'il y ait de closure dans votre fichier des routes sinon ça ne fonctionnera pas !

Maintenant, votre application sera plus rapide mais… si vous apportez une modification au fichier des routes, celle-ci ne sera plus prise en compte puisque c'est le cache qui sert de référence. Vous avez alors le choix entre supprimer le cache :

php artisan route:clear

et tout simplement le récréer directement avec la commande vue plus haut.

De toute façon, je ne vois pas trop l'intérêt d'utiliser un cache de routes en cours de développement : réservez donc cette action pour la production parce que là, vous n'aurez normalement plus de modification à apporter à vos routes. Toutefois, si vous devez ensuite faire une mise à jour, n'oubliez pas ce cache !

VIII-C-4-b - La configuration

Ce que j'ai dit ci-dessus pour les routes est tout aussi valable pour la configuration, et on dispose de ces deux commandes :

config:cache
config:clear

VIII-D - En résumé

- Laravel est équipé d'un système de gestion de fichiers unifié qui permet des manipulations en local ou sur le cloud.

- Laravel est équipé d'un système de cache unifié.

- Laravel autorise la mise en cache des routes.

IX - CSS et JavaScript

Laravel est un framework PHP consacré à la gestion côté serveur, mais on ne peut pas créer une application web sans générer du HTML, et pour l'accompagner, du CSS et du JavaScript. Laravel n'impose rien en la matière, mais il offre quelques outils en privilégiant Bootstrap et Vue.js. D'autres part, il propose NPM comme installeur.

Vous n'êtes évidemment pas obligé d'utiliser ces possibilités et vous pouvez gérer le côté client de façon classique, c'est d'ailleurs ce que j'ai fait pour l'application d'exemple. Je suis parti de templates existants avec leur code déjà généré que je me suis contenté de compléter de façon traditionnelle. D'autre part, la nature de ce qui est à gérer côté client, avec essentiellement un peu de manipulation du DOM et de l'Ajax, ne justifie pas à mes yeux l'utilisation d'un framework particulier, jQuery étant parfaitement adapté à ce genre de traitement.

Un article récent, et plutôt intéressant, montre comment passer de JQuery à Vue.js avec un exemple concret.

IX-A - Le CSS avec Laravel Mix

Si vous voulez utiliser un langage de génération de CSS comme Sass (qui connaît un grand succès) ou Less, alors vous pouvez opter pour Laravel Mix. Il est vrai que ces langages présentent de nombreux avantages par rapport au simple CSS :

- utilisation de variables ;

- imbrication de code ;

- importations ;

- mixins (groupe de déclaration CSS réutilisable) ;

- héritage ;

- opérateurs…

On peut très bien se contenter de la console pour générer le CSS à partir, par exemple, d'un fichier Sass :

sass input.scss output.css

On peut en plus mettre en place un observateur pour régénérer le code.

Mais on peut aussi utiliser Laravel Mix. Sous le capot, il utilise Webpack qui n'est pas si simple que ça à utiliser directement. Du coup, c'est une façon plus simple d'utiliser Webpack.

IX-A-1 - Installation

Pour utiliser « Laravel Mix », vous devez disposer de NPM, donc aussi de node.js. Si vous n'êtes pas trop sûr d'avoir ça sur votre machine, entrez ce qui suit dans votre console :

node -v
npm -v

Si vous obtenez un numéro de version, c'est que vous en disposez, mais assurez-vous quand même que ce ne soient pas des versions trop anciennes sinon vous aurez des soucis !

Si vous n'avez rien, alors installez-les en suivant les procédures sur les sites correspondants.

Dans l'installation de Laravel, vous trouverez déjà un fichier package.json à la racine :

Il comporte toutes les déclarations de scripts et de dépendances nécessaires. Selon vos besoins, vous pourrez adapter tout ça : par exemple, si vous ne comptez pas utiliser vue.js, vous pouvez le retirer de la liste des dépendances.

On peut remarquer la présence de bootstrap-sass :

@import "~bootstrap-sass/assets/stylesheets/bootstrap";

C'est une version « Sass » de « Bootstrap 3 ».

Pour l’installer, il ne vous reste plus qu'à entrer :

npm install

Là, vous avez le temps d'aller boire tranquillement un café…

Vous allez avoir la création d'un dossier node_modules contenant toutes les dépendances :

IX-A-2 - Utilisation

Maintenant vous pouvez utiliser le fichier webpack.mix.js pour préciser ce que vous voulez compiler. Par défaut on a ce code :

1. let mix = require('laravel-mix');

3. /*

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

5. | Mix Asset Management

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

7. |

8. | Mix provides a clean, fluent API for defining some Webpack build steps

9. | for your Laravel application. By default, we are compiling the Sass

10. | file for the application as well as bundling up all the JS files.

11. |

12. */

14. mix.js('resources/assets/js/app.js', 'public/js')

15. .sass('resources/assets/sass/app.scss', 'public/css');

On a la méthode sass (on dispose également de less et stylus) et on demande de compiler le fichier resources/assets/sass/app.scss dans public/css. On a effectivement ce fichier ici :

Pour lancer le processus en mode développement, on entre :

npm run dev

Ce qui a pour effet de créer (en fait ici régénérer parce qu'il existe déjà) le fichier app.css (et aussi celui du JavaScript mais j'en parlerai plus loin) :

Le code CSS est en clair puisqu'on est en mode développement :

1. html {

2. font-family: sans-serif;

3. -ms-text-size-adjust: 100%;

4. -webkit-text-size-adjust: 100%;

5. }

7. body {

8. margin: 0;

9. }

En mode production on utilise :

npm run production

Là, on obtient un code minifié pour gagner en dimension de fichier, donc en temps de téléchargement.

Un aspect intéressant est le mode observateur :

npm run watch

Dans ce mode, le code est surveillé et la compilation se lance à chaque changement, ce qui est parfait quand on développe.

IX-B - Le JavaScript avec Laravel Mix

Pour le JavaScript on peut aussi utiliser Laravel Mix comme on l'a vu ci-dessus pour le CSS. D'ailleurs, on a dans le fichier webpack.mix.js une compilation de JavaScript :

mix.js('resources/assets/js/app.js', 'public/js')

On utilise la méthode js pour compiler le fichier resources/assets/js/app.js dans public/js. On a effectivement ce fichier ici :

et le résultat compilé va se retrouver ici :

Les commandes sont strictement les mêmes que celles qu'on a vues pour le CSS puisque les opérations sont groupées.

Voyons de plus près le fichier resources/assets/js/app.js :

1. require('./bootstrap');

3. window.Vue = require('vue');

5. Vue.component('example', require('./components/Example.vue'));

7. const app = new Vue({

8. el: '#app'

9. });

On voit qu'on utilise vue.js et qu'on crée le composant example. Le code de ce composant est ici :

Pour information j'ai écrit un petit cours sur vue.js 2 dans ce blog.

Voici le code de ce composant :

1. <template>

2. <div class="container">

3. <div class="row">

4. <div class="col-md-8 col-md-offset-2">

5. <div class="panel panel-default">

6. <div class="panel-heading">Example Component</div>

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

9. I'm an example component!

10. </div>

11. </div>

12. </div>

13. </div>

14. </div>

15. </template>

17. <script>

18. export default {

19. mounted() {

20. console.log('Component mounted.')

21. }

22. }

23. </script>

On va s'en servir pour voir si ça fonctionne… Il suffit de créer une vue qui charge le CSS et le JavaScript, et qui inclue le composant de Vue.js :

1. <!DOCTYPE html>

2. <html>

3. <head>

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

5. <link rel="stylesheet" href="{{ asset('css/app.css') }}">

6. </head>

7. <body>

8. <div id="app">

9. <example></example>

10. </div>

11. <script type="text/javascript" src="{{ asset('js/app.js') }}"></script>

12. </body>

13. </html>

et voici le résultat :

Comme vous le constatez, l'intendance est en place si vous voulez l'utiliser !

Si vous préférez utiliser React, alors utilisez cette commande :

php artisan preset react

Il faut ensuite relancer :

npm install

Maintenant, si vous regardez le code du fichier resources/assets/js/app.js :

1. require('./bootstrap');

3. require('./components/Example');

c'est un composant React qui est appelé. Ce composant est ici :

Avec ce code :

1. import React, { Component } from 'react';

2. import ReactDOM from 'react-dom';

4. export default class Example extends Component {

5. render() {

6. return (

7. <div className="container">

8. <div className="row">

9. <div className="col-md-8 col-md-offset-2">

10. <div className="panel panel-default">

11. <div className="panel-heading">Example Component</div>

13. <div className="panel-body">

14. I'm an example component!

15. </div>

16. </div>

17. </div>

18. </div>

19. </div>

20. );

21. }

22. }

24. if (document.getElementById('example')) {

25. ReactDOM.render(<Example />, document.getElementById('example'));

26. }

Vous pouvez tester ce composant avec la même vue que ci-dessus puisqu'il a le même nom.

IX-C - Les actions de Laravel Mix

Laravel Mix sait faire bien d'autres choses que compiler du CSS ou du JavaScript.

Par exemple il sait copier des fichiers :

mix.copy('node_modules/foo/bar.css', 'public/css/bar.css');

et copier des dossiers :

mix.copyDirectory('assets/img', 'public/img');

je vous invite à consulter la documentation officielle pour en apprendre plus.

IX-D - En résumé

- Laravel Mix permet de compiler du CSS.

- Laravel Mix permet de compiler du JavaScript.

- Laravel comporte une intendance pour l'utilisation de Vue.js.

- Une commande Artisan permet de remplacer l'intendance de Vue.js pour celle de React.

X - Vue.js

Dans ce chapitre, je vous propose de voir un exemple simple d'utilisation d'un framework JavaScript, en l'occurrence Vue.js, comme gestionnaire côté client associé à Laravel.

Je vous propose de démarrer le projet à partir de celui que j'ai élaboré dans cet article avec Materialize (lien direct de téléchargement). Il y a un lien dans l'article pour vous permettre de télécharger le projet de départ.

Comme exemple je vous propose une simple page qui permet de laisser une pensée pour les utilisateurs enregistrés, donc un simple texte qu'on a envie de partager.

Ce n'est pas une initiation à Vue.js : pour ça, vous pouvez vous référer à mon cours ici. C'est juste pour montrer son utilisation avec Laravel.

Mettez à jour le fichier .env selon votre configuration. Vous pouvez changer le nom de l'application :

APP_NAME=Pensées

Vous pouvez télécharger le code final de ce chapitre ici.

D'autre part, j'ai mis en ligne l'application en démonstration ici. Vous pouvez y laisser vos pensées

X-A - Le serveur

X-A-1 - Données

On va donc créer une table pour mémoriser les pensées :

php artisan make:migration create_pensees_table

Changez ainsi le code de la méthode up :

1. public function up()

2. {

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

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

5. $table->timestamps();

6. $table->text('text');

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

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

9. });

10. }

puis lancez la migration :

php artisan migrate

Si tout s'est bien passé, vous devez avoir ces quatre tables :

On va aussi créer le modèle :

php artisan make:model Pensee

On va également établir les relations. Dans le modèle User on va avoir :

1. public function pensees()

2. {

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

4. }

et la réciproque dans le modèle Pensee :

1. public function user()

2. {

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

4. }

On va aussi penser à l'assignation de masse pour la création d'une pensée dans le modèle Pensee :

protected $fillable = [
'text', 'user_id',
];

On va un peu remplir nos tables. On commence par créer un factory pour les pensées :

php artisan make:factory PenseeFactory --model=Pensee

en complétant le code pour la génération du texte :

1. <?php

3. use Faker\Generator as Faker;

5. $factory->define(App\Pensee::class, function (Faker $faker) {

6. return [

7. 'text' => $faker->text,

8. ];

9. });

Lancez tinker :

php artisan tinker

On va maintenant créer six utilisateurs avec chacun une pensée :

factory(App\User::class, 6)->create()->each(function ($u) {$u->pensees()->save(factory(App\Pensee::class)->make());});

Voilà, pour les données on est parés !

X-A-2 - Contrôleur

On va créer un contrôleur de ressource pour nos pensées :

php artisan make:controller PenseeController --resource

On va ne garder que les méthodes index, create et destroy :

1. <?php

3. namespace App\Http\Controllers;

5. use Illuminate\Http\Request;

7. class PenseeController extends Controller

8. {

9. public function index()

10. {

11. //

12. }

14. public function store(Request $request)

15. {

16. //

17. }

19. public function destroy($id)

20. {

21. //

22. }

23. }

On va un peu coder tout ça en ajoutant une méthode initiale pour charger l'application :

1. <?php

3. namespace App\Http\Controllers;

5. use Illuminate\Http\Request;

6. use App\Pensee;

8. class PenseeController extends Controller

9. {

11. public function __construct()

12. {

13. $this->middleware('auth')->except('app', 'index');

14. }

16. public function app()

17. {

18. return view('pensees.index');

19. }

21. public function index(Request $request)

22. {

23. $pensees = Pensee::with('user')->latest()->get();

25. $user = auth()->check() ? auth()->id() : 0;

27. return response()->json([$pensees, $user]);

28. }

30. public function store(Request $request)

31. {

32. $request->validate([

33. 'text' => 'required|max:1000',

34. ]);

36. $request->merge(['user_id' => $request->user()->id]);

38. $pensee = Pensee::create($request->all());

40. return Pensee::with('user')->find($pensee->id);

41. }

43. public function destroy(Pensee $pensee)

44. {

45. $this->authorize('delete', $pensee);

47. $pensee->delete();

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

50. }

51. }

En gros, uniquement des choses qu'on a déjà vues dans ce cours…

La méthode merge permet d'ajouter des éléments à la requête.

X-A-3 - Protections

On doit protéger les URL qui sont réservées aux utilisateurs authentifiés. On va donc ajouter un constructeur dans le contrôleur pour ajouter le middleware auth pour les méthodes concernées :

1. public function __construct()

2. {

3. $this->middleware('auth')->except('app', 'index');

4. }

D'autre part, il faut faire en sorte que seul le rédacteur d'une pensée puisse supprimer cette pensée :

php artisan make:policy PenseePolicy --model=Pensee

On va garder seulement la méthode delete avec ce code :

1. <?php

3. namespace App\Policies;

5. use App\User;

6. use App\Pensee;

7. use Illuminate\Auth\Access\HandlesAuthorization;

9. class PenseePolicy

10. {

11. use HandlesAuthorization;

13. public function delete(User $user, Pensee $pensee)

14. {

15. return $user->id == $pensee->user_id;

16. }

17. }

Il faut l'enregistrer dans AuthServiceProvider :

1. use App\ { Pensee, Policies\PenseePolicy };

3. class AuthServiceProvider extends ServiceProvider

4. {

5. protected $policies = [

6. Pensee::class => PenseePolicy::class,

7. ];

9. ...

Et pour terminer on l'utilise dans le contrôleur :

1. public function destroy(Pensee $pensee)

2. {

3. $this->authorize('delete', $pensee);

5. $pensee->delete();

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

8. }

X-A-4 - Routes

On a aussi besoin des routes pour accéder aux méthodes du contrôleur qu'on vient de créer et pour un peu réorganiser. Remplacez toutes les routes existantes par celles-ci :

1. Auth::routes();

3. Route::get('/', 'PenseeController@app');

5. Route::resource('pensees', 'PenseeController', ['only' => [

6. 'index', 'store', 'destroy',

7. ]]);

Vous devez donc avoir toutes ces routes :

Notre serveur est maintenant prêt !

X-B - Le client

On va donc passer côté client maintenant…

Commencez par générer les modules avec NPM :

npm install

X-B-1 - Vue-resource

Par défaut, Vue.js n'est pas équipé pour gérer des requêtes HTTP. On va donc installer ce package :

Alors encore une petite commande :

npm install vue-resource

On va déclarer ce composant dans notre fichier resources/assets/js/app.js :

1. window.Vue = require('vue');

3. var VueResource = require('vue-resource');

4. Vue.use(VueResource);

6. Vue.http.headers.common['X-CSRF-TOKEN'] = document.head.querySelector('meta[name="csrf-token"]').content;

La dernière ligne a pour objectif de créer un « header » pour la protection CSRF. Normalement axios devrait le faire, mais pour une raison que j'ignore, ça ne fonctionne pas.

Maintenant on va pouvoir créer des requêtes HTTP vers notre serveur et recevoir des réponses : en gros, communiquer !

X-B-2 - La liste des pensées

Par défaut on a un composant nommé Exemple :

On va changer son nom en l'appelant par exemple App :

et donc renseigner en conséquence le fichier resources/assets/js/app.js qui doit donc finalement contenir ce code :

1. require('./bootstrap');

3. window.Vue = require('vue');

4. var VueResource = require('vue-resource');

5. Vue.use(VueResource);

7. Vue.component('app', require('./components/App.vue'));

9. const app = new Vue({

10. el: '#app'

11. });

On va avoir besoin d'une vue de départ :

avec ce simple code pour charger le composant de Vue.js :

1. @extends('layouts.app')

3. @section('content')

4. <div id="app">

5. <app></app>

6. </div>

7. @endsection

X-B-2-a - Une liste simple

Pour le composant App.vue, on va commencer par ce code :

1. <template>

2. <div class="container">

3. <div v-for="pensee in pensees">

4. <h4>{{ pensee.user.name }}</h4>

5. <p>{{ pensee.text }}</p>

6. <p>{{ pensee.created_at }}</p>

7. </div>

8. </div>

9. </template>

11. <script>

12. export default {

13. resource: null,

14. data () {

15. return {

16. pensees: {}

17. }

18. },

19. mounted () {

20. this.resource = this.$resource('/pensees{/id}')

21. this.resource.get().then((response) => {

22. this.pensees = response.body

23. })

24. }

25. }

26. </script>

Au niveau des data, une simple variable pensees pour contenir les pensées.

Lorsque le composant est prêt (mounted), on lance la requête GET /pensees et on met le résultat dans pensees.

Dans le template, la directive v-for se charge de générer le HTML.

Si tout se passe bien, vous devez obtenir cet aspect :

X-B-2-b - On améliore l'aspect

On va un peu améliorer l'aspect obtenu. On va créer un composant Card :

avec ce code :

1. <template>

2. <div class="card red lighten-2">

3. <div class="card-content white-text">

4. <span class="card-title">{{ name }}</span>

5. <p>{{ text }}</p>

6. <p><small>{{ date }}</small></p>

7. </div>

8. </div>

9. </template>

11. <script>

12. export default {

13. props: ['name', 'text', 'date']

14. }

15. </script>

et on va changer ainsi le code du composant App :

1. <template>

2. <div class="container">

3. <div v-for="pensee in pensees">

4. <card :name="pensee.user.name" :text="pensee.text" :date="pensee.created_at"></card>

5. </div>

6. </div>

7. </template>

9. <script>

10. import Card from './Card'

12. export default {

13. resource: null,

14. data () {

15. return {

16. pensees: []

17. }

18. },

19. mounted () {

20. this.resource = this.$resource('/pensees{/id}')

21. this.resource.get().then((response) => {

22. this.pensees = response.body

23. })

24. },

25. components: {

26. Card

27. }

28. }

29. </script>

Le code est mieux organisé et l'aspect plus présentable :

X-B-2-c - Suppression d'une pensée

Maintenant comment faire si un utilisateur veut supprimer une de ses pensées ? Il faudrait prévoir un bouton quelque part qui n’apparaîtrait que pour le rédacteur de la pensée, mais pour le moment, rien ne nous indique que l'utilisateur actuel est le rédacteur de certaines pensées, parce qu'on ne dispose pas de son identifiant…

On va donc modifier la méthode index du contrôleur :

1. public function index(Request $request)

2. {

3. $pensees = Pensee::with('user')->oldest()->get();

5. $user = auth()->check() ? auth()->id() : 0;

7. return response()->json([$pensees, $user]);

8. }

Ainsi, on renvoie les pensées et l'identifiant de l'utilisateur en cours (0 s'il n'y en a pas).

Voici le nouveau composant App :

1. <template>

2. <div class="container">

3. <div v-for="pensee in pensees">

4. <card :pensee="pensee" :user="user" @deletePensee="deletePensee"></card>

5. </div>

6. </div>

7. </template>

9. <script>

10. import Card from './Card'

12. export default {

13. resource: null,

14. data () {

15. return {

16. pensees: [],

17. user: 0

18. }

19. },

20. mounted () {

21. this.resource = this.$resource('/pensees{/id}')

22. this.resource.get().then(response => {

23. this.pensees = response.body[0]

24. this.user = response.body[1]

25. })

26. },

27. components: {

28. Card

29. },

30. methods: {

31. deletePensee (id) {

32. this.resource.delete({id: id}).then(response => {

33. let index = _.findIndex(this.pensees, function(o) { return o.id == id; })

34. this.pensees.splice(index, 1)

35. })

36. }

37. }

38. }

39. </script>

On a le nouveau data user pour mémoriser l'identifiant de l'utilisateur.

On envoie dans le composant Card l'objet pensee et user.

On attend un événement (deletePensee) de la part de Card. Cet événement a pour effet d'activer la méthode deletePensee qui reçoit l'identifiant de la pensée. On envoie alors la requête pour supprimer cette pensée sur le serveur, et au retour on la supprime en local.

Voici le nouveau code pour Card :

1. <template>

2. <div class="card red lighten-2">

3. <div class="card-content white-text">

4. <span class="card-title">{{ pensee.user.name }}</span>

5. <p>{{ pensee.text }}</p>

6. <p><small>{{ pensee.created_at }}</small></p>

7. </div>

8. <div v-if="deletable" class="card-action">

9. <a href="#" @click.prevent="deletePensee">Supprimer cette pensée</a>

10. </div>

11. </div>

12. </template>

14. <script>

15. export default {

16. props: ['pensee', 'user'],

17. computed: {

18. deletable () {

19. return this.pensee.user_id == this.user

20. }

21. },

22. methods: {

23. deletePensee () {

24. this.$emit('deletePensee', this.pensee.id)

25. }

26. }

27. }

28. </script>

On reçoit maintenant un objet global pensee et aussi user.

On fait apparaître le lien de suppression avec un computed qui teste l'utilisateur.

Pour la suppression, on envoie un événement ($emit) au parent App.

X-B-2-d - Ajout d'une pensée

Il faut aussi pouvoir ajouter une pensée, et donc un formulaire pour la saisie, réservé aux utilisateurs connectés. On va utiliser une fenêtre modale pour le réaliser.

On va ajouter un élément dans le menu et l'activation de la fenêtre modale (resources/views/layouts/app.blade.php). Voici le code complet de la vue résultante :

1. <!DOCTYPE html>

2. <html lang="{{ app()->getLocale() }}">

3. <head>

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

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

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

8. <!-- CSRF Token -->

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

11. <title>{{ config('app.name', 'Laravel') }}</title>

13. <!-- Styles -->

14. <link href="{{ asset('css/app.css') }}" rel="stylesheet">

15. @yield('css')

16. </head>

17. <body>

18. <div id="app">

20. @auth

21. <ul id="dropdown1" class="dropdown-content">

22. <li>

23. <a href="{{ route('logout') }}"

24. onclick="event.preventDefault();

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

26. Deconnexion

27. </a>

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

30. {{ csrf_field() }}

31. </form>

32. </li>

33. <li>

34. <a class="modal-trigger" href="#ajout">Une pensée</a>

35. </li>

36. </ul>

37. @endauth

39. <nav>

40. <div class="nav-wrapper">

41. <a href="{{ url('/') }}" class="brand-logo">&nbsp;{{ config('app.name', 'Laravel') }}</a>

42. <a href="#" data-activates="mobile-demo" class="button-collapse"><i class="material-icons">menu</i></a>

43. @guest

44. <ul class="right hide-on-med-and-down">

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

46. <li><a href="{{ route('register') }}">Enregistrement</a></li>

47. </ul>

48. <ul class="side-nav" id="mobile-demo">

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

50. <li><a href="{{ route('register') }}">Enregistrement</a></li>

51. </ul>

52. @else

53. <ul class="right hide-on-med-and-down">

54. <li><a class="dropdown-button" href="#!" data-activates="dropdown1">{{ Auth::user()->name }}<i class="material-icons right">arrow_drop_down</i></a></li>

55. </ul>

56. @endguest

57. </div>

58. </nav>

59. @yield('content')

60. </div>

62. <!-- Scripts -->

63. <script src="{{ asset('js/app.js') }}"></script>

64. <script>

65. $(".button-collapse").sideNav()

66. $(".dropdown-button").dropdown()

67. $('.modal').modal()

68. </script>

69. </body>

70. </html>

On ajoute le code de la fenêtre modale dans le composant App, ainsi que sa gestion (mise à jour, validation…) :

1. <template>

2. <div class="container">

3. <div v-for="pensee in pensees" :key="pensee.id">

4. <card :pensee="pensee" :user="user" @deletePensee="deletePensee"></card>

5. </div>

6. <div id="ajout" class="modal">

7. <form v-on:submit.prevent="addPensee">

8. <div class="modal-content">

9. <h4>Ajout d'une pensée</h4>

10. <hr>

11. <div class="input-field col s12">

12. <textarea id="pensee" v-model="texte" class="materialize-textarea"></textarea>

13. <label for="pensee">Entrez ici votre pensée</label>

14. <p class="red-text">{{ error }}</p>

15. </div>

16. </div>

17. <div class="modal-footer">

18. <button class="btn waves-effect waves-light" type="submit">Envoyer

19. <i class="material-icons right">send</i>

20. </button>

21. </div>

22. </form>

23. </div>

24. </div>

25. </template>

27. <script>

28. import Card from './Card'

30. export default {

31. resource: null,

32. data () {

33. return {

34. error: '',

35. texte: '',

36. pensees: [],

37. user: 0

38. }

39. },

40. mounted () {

41. this.resource = this.$resource('/pensees{/id}')

42. this.resource.get().then(response => {

43. this.pensees = response.body[0]

44. this.user = response.body[1]

45. })

46. },

47. components: {

48. Card

49. },

50. methods: {

51. deletePensee (id) {

52. this.resource.delete({id: id}).then(response => {

53. let index = _.findIndex(this.pensees, function(o) { return o.id == id; })

54. this.pensees.splice(index, 1)

55. })

56. },

57. addPensee () {

58. this.resource.save({ text: this.texte }).then(response => {

59. $('#ajout').modal('close')

60. this.texte = ''

61. this.error = ''

62. this.pensees.unshift(response.body)

63. }, response => {

64. this.error = response.body.errors.text[0]

65. })

66. }

67. }

68. }

69. </script>

et maintenant, on peut ajouter des pensées !

X-B-2-e - Les dates en français

Vous avez sans doute remarqué que le format des dates est celui par défaut, lequel n'est pas très adapté au français. On pourrait changer ça au niveau de Laravel, mais on va le faire avec JavaScript.

On va utiliser la superbe librairie moment (l'équivalent de Carbon pour JavaScript) :

npm install moment

et on l'utilise dans le composant Card :

1. <template>

2. <div class="card red lighten-2">

3. <div class="card-content white-text">

4. <span class="card-title">{{ pensee.user.name }}</span>

5. <p>{{ pensee.text }}</p>

6. <p><small>{{ date }}</small></p>

7. </div>

8. <div v-if="deletable" class="card-action">

9. <a href="#" @click.prevent="deletePensee">Supprimer cette pensée</a>

10. </div>

11. </div>

12. </template>

14. <script>

15. import moment from 'moment'

16. moment.locale('fr')

18. export default {

19. props: ['pensee', 'user'],

20. computed: {

21. deletable () {

22. return this.pensee.user_id == this.user

23. },

24. date () {

25. return moment(pensee.created_at).format('D MMMM YYYY à H:mm:ss')

26. }

27. },

28. methods: {

29. deletePensee () {

30. this.$emit('deletePensee', this.pensee.id)

31. }

32. }

33. }

34. </script>

On importe la librairie :

import moment from 'moment'

On fixe la locale :

moment.locale('fr')

On crée un élément calculé :

date () {
return moment(pensee.created_at).format('D MMMM YYYY à H:mm:ss')
}

et on l'utilise dans le template :

<p><small>{{ date }}</small></p>

Maintenant, on a les dates en bon français :

X-B-2-f - La pagination

Si on commence à avoir beaucoup de pensées, il va nous falloir une pagination :

npm install vue-paginate

et voilà le code modifié en conséquence pour le composant App :

1. <template>

2. <div class="container">

3. <paginate name="pensees" :list="pensees" :per="3">

4. <li v-for="pensee in paginated('pensees')" :key="pensee.id">

5. <card :pensee="pensee" :user="user" @deletePensee="deletePensee"></card>

6. </li>

7. </paginate>

8. <paginate-links for="pensees" :classes="{'ul': 'pagination'}"></paginate-links>

9. <div id="ajout" class="modal">

10. <form v-on:submit.prevent="addPensee">

11. <div class="modal-content">

12. <h4>Ajout d'une pensée</h4>

13. <hr>

14. <div class="input-field col s12">

15. <textarea id="pensee" v-model="texte" class="materialize-textarea"></textarea>

16. <label for="pensee">Entrez ici votre pensée</label>

17. <p class="red-text">{{ error }}</p>

18. </div>

19. </div>

20. <div class="modal-footer">

21. <button class="btn waves-effect waves-light" type="submit">Envoyer

22. <i class="material-icons right">send</i>

23. </button>

24. </div>

25. </form>

26. </div>

27. </div>

28. </template>

30. <script>

31. import Card from './Card'

32. import VuePaginate from 'vue-paginate'

34. export default {

35. resource: null,

36. data () {

37. return {

38. error: '',

39. texte: '',

40. pensees: [],

41. paginate: ['pensees'],

42. user: 0

43. }

44. },

45. mounted () {

46. this.resource = this.$resource('/pensees{/id}')

47. this.resource.get().then(response => {

48. this.pensees = response.body[0]

49. this.user = response.body[1]

50. })

51. },

52. components: {

53. Card

54. },

55. methods: {

56. deletePensee (id) {

57. this.resource.delete({id: id}).then(response => {

58. let index = _.findIndex(this.pensees, function(o) { return o.id == id; })

59. this.pensees.splice(index, 1)

60. })

61. },

62. addPensee () {

63. this.resource.save({ text: this.texte }).then(response => {

64. $('#ajout').modal('close')

65. this.texte = ''

66. this.error = ''

67. this.pensees.unshift(response.body)

68. }, response => {

69. this.error = response.body.errors.text[0]

70. })

71. }

72. }

73. }

74. </script>

On a maintenant une pagination :

On pourrait encore améliorer cette application, mais on en restera là…

X-C - En résumé

Laravel n'impose rien au niveau de la gestion côté client, et ce ne sont pas les solutions qui manquent. Toutefois, l'installation de Laravel prévoit par défaut une intendance pour Vue.js. On a vu dans ce chapitre que cette intendance est au point et que tout fonctionne de façon efficace. La prise en main de Vue.js est moins difficile que d'autres framework comme « Angular ».

Reste que le choix des outils de gestion côté client n'est pas évident. Souvent, JQuery est amplement suffisant lorsqu'on a juste à générer des requêtes HTTP et à manipuler un peu le DOM. À quel moment devient-il plus intéressant de passer à un framework plus complet ? La réponse n'est pas évidente parce qu’elle dépend de plusieurs facteurs parmi lesquels la maîtrise qu'on a d'un outil particulier n'est pas le moindre.

Si vous n'avez jamais utilisé Vue.js, je vous conseille de l'essayer parce qu'il est simple et puissant et que sa courbe d'apprentissage n'est pas rebutante.