Tutoriel pour apprendre à utiliser le framework Laravel 5.3

Plus loin

Maurice Chavelli

Pour cette partie du cours nous allons utiliser une application d'exemple. Elle va nous donner l'occasion de développer ce que nous avons déjà vu et d'aborder d'autres points importants. Elle va éviter de trop charger ce cours de lignes de code.

Commentez

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

- Auteur : Maurice Chavelli

- Parution : 20 août 2017

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

Sommaire

- I. Une application d'exemple

- I-A. Installation

- I-A-1. git clone https://github.com/bestmomo/laravel5-3-example.git projectname

- I-A-2. cd projectname

- I-A-3. composer install

- I-A-4. composer update

- I-A-5. copy .env.example to .env

- I-A-6. php artisan key:generate

- I-A-7. if you use MySQL in .env file

- I-A-8. php artisan migrate -seed to create and populate tables

- I-A-9. edit .env for emails configuration

- I-A-10. optionaly npm install to manage assets

- I-B. Aspect du site

- I-B-1. Le formulaire de contact‌

- I-B-2. Le blog

- I-B-3. Les langues

- I-B-4. La connexion

- I-C. L'administration

- I-D. En résumé

- II. Des vues propres

- II-A. Le problème

- II-B. Organisation du code

- II-C. Un composant pour case à cocher

- II-D. Un composant pour liste de choix

- III. La localisation

- III-A. Le principe

- III-A-1. La façade Lang et la classe Translator

- III-A-2. Les fichiers de langage

- III-A-3. Le fonctionnement

- III-B. Un middleware ou un événement

- III-C. Le changement de langue

- III-D. Les dates

- III-D-1. Le presenter

- III-E. Les vues

- III-F. Les noms pour la validation

- III-G. En résumé

- IV. Les événements

- IV-A. Le design pattern Observateur

- IV-B. Organisation du code

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

- IV-B-2. Le fournisseur de service (service provider)

- IV-C. Le statut de l'utilisateur dans l'application

- IV-C-1. La connexion

- IV-C-2. La deconnexion

- IV-C-3. Accès à une page

- IV-D. En résumé

- V. La sécurité

- V-A. L'authentification

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

- V-B-1. Les injections SQL

- V-B-2. CSRF

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

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

- V-C. Les requêtes de formulaire et le pot de miel

- V-D. Les autorisations

- V-E. En résumé

- VI. Les notifications

- VI-A. Organisation du code

- VI-B. La confirmation de l'adresse email

- VI-B-1. Le contrôleur RegisterController

- VI-B-2. La notification

- VI-B-3. L'email

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

- VI-C-1. La table des notifications

- VI-C-2. La notification

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

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

- VI-C-5. La vue index

- VI-D. En résumé

- VII. Ajax

- VII-A. Les messages dans l'application

- VII-B. La vue des messages

- VII-C. Le JavaScript

- VII-C-1. La protection CSRF

- VII-D. Le traitement côté serveur

- VII-D-1. Le contrôleur

- VII-D-2. Le middleware

- VII-E. En résumé

- VIII. Les tests unitaires

- VIII-A. L'intendance des tests

- VIII-A-1. PHPUnit

- VIII-A-2. L'intendance de Laravel

- VIII-A-3. L'environnement de test

- VIII-B. Construire un test

- VIII-B-1. Les trois étapes d'un test

- VIII-C. Assertions et appel de routes

- VIII-C-1. Les assertions

- VIII-C-2. Appel de route et test de réponse

- VIII-D. Les vues et les contrôleurs

- VIII-D-1. Les vues

- VIII-D-2. Les contrôleurs

- VIII-E. Isoler les tests

- VIII-E-1. Simuler une classe

- VIII-F. Tester une application

- VIII-F-1. Visite et clic sur un lien

- VIII-F-2. Formulaire

- VIII-G. En résumé

- IX. Le déploiement

- IX-A. L'environnement

- IX-A-1. Créer des environnements

- IX-B. Le déploiement

- IX-B-1. Le serveur

- IX-B-2. L'envoi des fichiers de l'application

- IX-B-3. L'installation

- IX-B-4. Le cache‌

- IX-B-5. La base de données

- IX-C. Les mises à jour ultérieures

- IX-C-1. Mise à jour du framework

- IX-C-2. Mise à jour de l'application

- IX-D. Installation sur serveur mutualisé

- IX-E. Créer une installation comme WordPress

- IX-E-1. Étape 1 : vérification du serveur

- IX-E-2. Étape 2 : la base de données

- IX-E-3. Étape 3 (optionnelle) : création d'un administrateur

- IX-F. En résumé

- X. Remerciements

I - Une application d'exemple

La première chose que vous avez à faire est donc d'installer cette application sur un serveur local et de regarder comment elle fonctionne.

I-A - Installation

L'installation est détaillée sur la page Github du dépôt. Je vais toutefois apporter quelques commentaires parce que la procédure est tout de même assez concise.

I-A-1 - git clone https://github.com/bestmomo/laravel5-3-example.git projectname

Ici on vous demande de cloner le dépôt sur votre machine dans le dossier "projectname". Vous pouvez évidemment changer le nom du dossier à votre convenance.

Si vous ne disposez pas de git sur votre machine vous avez le choix : soit l'installer, soit utiliser une autre méthode.

Comme autre possibilité vous pouvez télécharger le fichier compressé (zip) :

Il ne vous reste plus alors qu'à le décompresser dans votre dossier de projet.

I-A-2 - cd projectname

En partant du principe que vous avez effectué la commande git vous vous trouvez dans le dossier parent de votre projet.

La commande cd (Change Directory) a pour seul but de changer de dossier dans la console.

Si vous avez téléchargé le fichier compressé vous devez naviguer dans la console jusqu'à votre dossier.

I-A-3 - composer install

C'est la commande de Composer que nous avons déjà vue qui permet d'installer toutes les dépendances et générer les fichiers pour le chargement automatique des classes (autoload) :

Composer est assez malin pour garder les bibliothèques en cache pour éviter de les recharger.

I-A-4 - composer update

Lors de l'installation il se peut que certains composants ne soient pas installés dans leur dernière version, alors on lance une mise à jour pour arranger ça éventuellement.

I-A-5 - copy .env.example to .env

On a déjà vu qu'il faut créer un fichier .env à partir du fichier .env.example de l'installation de base :

I-A-6 - php artisan key:generate

Il y a déjà une clé présente dans le fichier .env, mais par sécurité, on demande de la régénérer.

Pour mémoire c'est cette clé qui assure la sécurité de l'encryptage.

I-A-7 - if you use MySQL in .env file

Comme on ne va pas utiliser sqlite on va créer une base MySQL et renseigner les variables, par exemple :

1. DB_CONNECTION=mysql

2. DB_HOST=127.0.0.1

3. DB_PORT=3306

4. DB_DATABASE=projectname

5. DB_USERNAME=root

6. DB_PASSWORD=

I-A-8 - php artisan migrate -seed to create and populate tables

Là on crée les tables et on les remplit :

I-A-9 - edit .env for emails configuration

Pour l'envoi des courriels vous devez renseigner les variables dans le fichier .env, par défaut on a mailtrap dont je vous ai déjà parlé dans ce cours :

1. MAIL_DRIVER=smtp

2. MAIL_HOST=mailtrap.io

3. MAIL_PORT=2525

4. MAIL_USERNAME=null

5. MAIL_PASSWORD=null

6. MAIL_ENCRYPTION=null

I-A-10 - optionaly npm install to manage assets

Si vous voulez apporter des modifications au CSS du site alors installez les dépendances de gulp comme on l'a vu dans le chapitre correspondant de ce cours.

I-B - Aspect du site

Après tout ça vous devriez aboutir sur la page d'accueil :

Si ce n'est pas le cas, vous n'avez plus qu'à reprendre depuis le début en trouvant où vous vous êtes trompé.

I-B-1 - Le formulaire de contact‌

Le site dispose d'un formulaire de contact :

I-B-2 - Le blog

Il y a aussi un blog avec un zone de saisie pour la recherche textuelle :

Pour chaque article du blog on a des tags et des commentaires :

I-B-3 - Les langues

L'interface est en trois langues :

I-B-4 - La connexion

Il y a un formulaire de connexion :

Il y a de la même manière un formulaire pour l'enregistrement et un autre pour le renouvellement du mot de passe.

On a trois types d'utilisateurs :

- les administrateurs (admin) qui ont tous les droits ;

- les rédacteurs (redac) qui peuvent rédiger et modifier des messages ;

- les utilisateurs (user) de base qui peuvent écrire des commentaires.

I-C - L'administration

Pour se connecter en tant qu'administrateur il faut entrer :

- login : admin@la.fr ;

- mot de passe : admin.

La barre de menu se modifie :

En cliquant sur ADMINISTRATION on arrive sur la page de l'administration :

Ici on peut gérer :

- les utilisateurs ;

- les rôles ;

- les messages ;

- les commentaires ;

- les médias ;

- les articles.

Pour se connecter en tant que rédacteur il faut entrer :

- login : redac@la.fr ;

- mot de passe : redac.

La barre de menu se modifie :

En cliquant sur REDACTION on arrive sur la page de la rédaction :

Ici on peut gérer seulement les articles et les médias.

Il serait trop laborieux de décrire toutes les fonctionnalités ici. Nous en verrons peu à peu dans cette partie du cours.

I-D - En résumé

- L'application d'exemple présente un site complet et fonctionnel.

- Cet exemple va nous permettre d'étudier de nouvelles fonctionnalités de Laravel tout en les voyant en action dans une application‌.

II - Des vues propres

On a vu comment créer des classes bien organisées et se contentant d'effectuer leur tâche. Par contre au niveau des vues c'est une autre histoire. En général on utilise un framework CSS, par exemple Bootstrap dont je me suis servi dans les exemples de ce cours. Mais que se passe-t-il le jour où ce framework évolue ou si on décide d'en changer ?

On a vu que le composant laravelcollective/html (même s'il na pas que des partisans), permet de simplifier l'écriture du code des vues. Malgré tout il reste quand même toutes les classes nécessaires pour le framework CSS, et elles peuvent être nombreuses, par exemple pour Bootstrap.

Dans l'application d'exemple j'ai choisi d'utiliser la possibilité de laravelcollective/html de créer des composants à partir de templates pour obtenir des méthodes plus puissantes. Ce chapitre va décrire ce qui a été réalisé.

II-A - Le problème

Lorsque l'on a vu les ressources on a été amenés à écrire ce genre de code pour un formulaire :

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

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

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

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

5. </div>

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

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

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

9. </div>

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

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

Il est facile de voir qu'il y a des répétitions de code pour les deux contrôles.

Dans les vues de l'application vous allez trouver plutôt ce genre de code pour un formulaire :

1. {!! Form::open(['url' => 'contact']) !!}

2. <div class="row">

3. {!! Form::controlBootstrap('text', 6, 'name', $errors, trans('front/contact.name')) !!}

4. {!! Form::controlBootstrap('email', 6, 'email', $errors, trans('front/contact.name')) !!}

5. {!! Form::submitBootstrap(trans('front/form.send'), 'col-lg-12') !!}

6. </div>

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

Pourtant le code résultant sera sensiblement le même. Si vous cherchez la méthode controlBootstrap dans le composant laravelcollective/html vous n'allez pas la trouver, elle a été créée pour l'application.

II-B - Organisation du code

La notion de « service » est suffisamment générale pour inclure la plupart des tâches à réaliser dans une application. Il a donc été créé un service pour gérer les fonctionnalités pour les vues, avec un fichier FormBuilder.php dans le dossier app/Services, pour mettre le code dont nous avons besoin :

Nous y trouvons une classe comportant les définitions des composants.

Les templates correspondants se trouvent rangés avec les vues dans le dossier components :

Enfin tout ça est initialisé dans le fournisseur de services (service provider) app/Providers/AppServiceProvider :

1. <?php

3. namespace App\Providers;

5. use Illuminate\Support\ServiceProvider;

6. use App\Services\FormBuilder;

8. class AppServiceProvider extends ServiceProvider

9. {

10. public function boot()

11. {

12. FormBuilder::boot();

13. }

15. ...

16. }

On utilise la méthode boot pour être sûr que tout a déjà été initialisé dans les méthodes register de tous les fournisseurs de services, en particulier pour que l'on dispose de la façade Form.

II-C - Un composant pour case à cocher

Pour comprendre comment tout ça est articulé on va prendre le cas de l'un des six composants définis que j'ai appelé checkboxBootstrap.

Dans la classe App/Services/FormBuilder on trouve sa définition :

1. <?php

3. namespace App\Services;

5. use Form;

7. class FormBuilder

8. {

9. public static function boot()

10. {

11. ...

12. Form::component('checkboxBootstrap', 'components.checkbox', [

13. 'name',

14. 'label',

15. ]);

16. ...

17. }

18. }

On utilise la méthode component :

- le premier paramètre est le nom de l'élément (checkboxBootstrap) ;

- le second paramètre est destiné à localiser le fichier contenant le template, ici il sera donc localisé en resources/views/components/checkbox.blade.php.

Le dernier paramètre est un tableau qui permet de transmettre toutes les valeurs pour renseigner la vue. On peut définir des valeurs par défaut, ce qui n'est pas le cas ici.

On peut donc facilement créer le template à partir de tout ça :

1. <div class="checkbox col-lg-12">

2. <label>

3. {!! Form::checkbox($name) !!}

4. {{ $label }}

5. </label>

6. </div>

Ensuite on peut directement l'utiliser dans une vue, par exemple dans celle de la connexion (resources/views/auth/login.blade.php) :

{!! Form::checkboxBootstrap('memory', trans('front/login.remind')) !!}

Ce qui correspond à la case à cocher « Se rappeler de moi » :

Cela se traduit finalement par ce code :

1. <div class="checkbox col-lg-12">

2. <label>

3. <input name="memory" type="checkbox" value="1">

4. Se rappeler de moi

5. </label>

6. </div>

C'est une procédure simple et très propre.

II-D - Un composant pour liste de choix

Voyons maintenant un exemple avec une liste de choix : selectBootstrap.

Dans la classe App/Services/FormBuilder on trouve sa définition :

1. Form::component('selectBootstrap', 'components.select', [

2. 'name',

3. 'list' => [],

4. 'selected' => null,

5. 'label' => null,

6. ])

Dans ce cas on a des éléments dans le tableau à transmettre à la vue dont les trois derniers ont une valeur par défaut.

On peut donc facilement créer le template à partir de tout ça :

1. <div class="form-group" style="width:200px;">

2. @if($label)

3. {!! Form::label($name, $label, ['class' => 'control-label']) !!}

4. @endif

5. {!! Form::select($name, $list, $selected, ['class' => 'form-control']) !!}

6. </div>

Remarquez que l'on peut utiliser toutes les possibilités de Blade.

On a une exemple d'utilisation dans le formulaire de création d'un utilisateur dans la partie administration pour le choix du rôle (resources/views/back/users/create.blade.php‌) :

{!! Form::selectBootstrap('role', $select, null, trans('back/users.role')) !!}

Avec cet aspect déroulé :

Avec ce code généré :

1. <div class="form-group" style="width:200px;">

2. <label for="role" class="control-label">Rôle</label>

3. <select class="form-control" id="role" name="role">

4. <option value="1">Administrator</option>

5. <option value="2">Redactor</option>

6. <option value="3">User</option>

7. </select>

8. </div>

C'est pas mal avec une seule ligne de code dans la vue !

En résumé, le composant laravelcollective/html permet de créer des composants avec une définition et un template.

III - La localisation

Lorsque l'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 avec l'application d'exemple.

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.

III-A - Le principe

III-A-1 - La façade Lang et la classe Translator

Laravel est équipé de la façade Lang pour générer des chaînes de caractères dans plusieurs langues. Mais vous savez qu'une façade cache une classe.

Laquelle dans notre cas ?

Toutes les classes qui sont concernées par la traduction se trouvent dans le dossier Illuminate\Translation :

On y trouve très logiquement :

- un service provider pour faire toutes les initialisations ;

- un fichier composer.json pour les dépendances ;

- une classe FileLoader avec son interface LoaderInterface pour gérer les fichiers de traduction ;

- une classe ArrayLoader pour charger les messages ;

- la classe principale Translator qui est mise en action par la façade.

III-A-2 - Les fichiers de langage

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, que nous avons déjà utilisé dans ce cours.

L'application d'exemple embarque trois langues : anglais, français et portugais (Brésil) :

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' => '« Previous',

3. 'next'     => 'Next »',

4. ];

On voit que l'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' => '« Précédent',

3. 'next'     => 'Suivant »',

4. ];

On retrouve évidemment les mêmes clés avec des valeurs adaptées au langage. Et c'est la même chose pour le portugais :

1. return [

2. 'previous' => '« Anterior',

3. 'next'     => 'Próxima »',

4. ];

III-A-3 - Le fonctionnement

On dispose de quelques méthodes pour tester et récupérer ces valeurs.

Avec :

Lang::get('pagination.previous');

je vais obtenir :

« Previous

Donc la version anglaise.

Comment faire pour obtenir la version française ?

Regardez dans le fichier config/app.php :

'locale' => 'en',

C'est ici qu'est fixé le langage en cours. Changez la valeur :

'locale' => 'fr',

Maintenant avec le même code vous allez obtenir :

« Précédent

Évidemment on va pouvoir changer cette valeur dans le code avec la méthode setLocale :

App::setLocale('fr');

Comme vous aurez de multiples endroits dans le code où vous allez récupérer des valeurs, il existe un helper :

trans('pagination.previous');

On peut aussi vérifier qu'une clé existe avec la méthode has :

Lang::has('pagination.previous');

Ce sont les méthodes fondamentales qui seront suffisantes pour le présent chapitre, vous pouvez trouver des compléments d'information dans la documentation.

III-B - Un middleware ou un événement

Quand un utilisateur choisit une langue il faut s'en rappeler, on va donc utiliser la session pour mémoriser son choix.

Lorsqu'un utilisateur débarque sans avoir encore choisi il faut lui présenter une langue, mais laquelle ?

Il est possible d'aller chercher l'information de la langue utilisée au niveau de la requête.

Maintenant la question est : où allons-nous placer le code pour gérer tout ça ?

Par exemple, un middleware pourrait s'en occuper. Voici un schéma :

Lorsque la requête arrive elle est prise en charge par le middleware. Là on va regarder si l'utilisateur a une session active avec une localisation, si c'est le cas on applique cette locale et on envoie la requête à l'étape suivante. Si ce n'est pas le cas, on regarde dans le header de la requête la langue envoyée par le navigateur, on la mémorise dans la session et on l'applique. On envoie alors la requête à l'étape suivante.

Ce middleware aurait ce code :

1. <?php

3. namespace App\Http\Middleware;

5. use Closure;

7. class Locale

8. {

9. protected $languages = ['en','fr','pt-BR'];

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

12. {

13. if(!session()->has('locale')) {

14. session()->put('locale', $request->getPreferredLanguage($this->languages));

15. }

17. app()->setLocale(session('locale'));

19. return $next($request);

20. }

21. }

Ce n'est pas ce qui a été réalisé dans l'application d'exemple. On y utilise un événement (nous verrons les événements dans un prochain chapitre) pour détecter l'arrivée d'un utilisateur et on utilise alors un service :

On a dans ce service un code proche de celui vu ci-dessus pour le middleware :

1. <?php

3. namespace App\Services;

5. use Request;

7. class Locale

8. {

9. /**

10. * Set the locale.

11. *

12. * @return void

13. */

14. public static function setLocale()

15. {

16. if (!session()->has('locale')) {

17. session()->put('locale', Request::getPreferredLanguage(config('app.languages')));

18. }

20. app()->setLocale(session('locale'));

21. }

22. }

La différence c'est que les langues sont définies dans la configuration, ce qui est plus logique :

'languages' => ['en', 'fr', 'pt-BR'],

III-C - Le changement de langue

Dans l'application d'exemple on peut choisir librement sa langue dans le menu si le fonctionnement automatique ne convient pas :

La liste déroulante est générée avec ce code :

1. <li class="dropdown">

2. <a data-toggle="dropdown" class="dropdown-toggle" href="#"><img width="32" height="32" alt="{{ session('locale') }}"  src="{!! asset('img/' . session('locale') . '-flag.png') !!}" />  <b class="caret"></b></a>

3. <ul class="dropdown-menu">

4. @foreach ( config('app.languages') as $user)

5. @if($user !== config('app.locale'))

6. <li><a href="{!! url('language') !!}/{{ $user }}"><img width="32" height="32" alt="{{ $user }}" src="{!! asset('img/' . $user . '-flag.png') !!}"></a></li>

7. @endif

8. @endforeach

9. </ul>

10. </li>

Et la route est celle-ci :

Route::get('language/{lang}', 'LanguageController')
->where('lang', implode('|', config('app.languages')));

Au passage remarquez la contrainte de route pour le paramètre qui doit être l'une des langues présentes dans la configuration.

On aboutit dans le contrôleur LanguageController :

1. <?php

3. namespace App\Http\Controllers;

5. class LanguageController extends Controller

6. {

7. /**

8. * Change language.

9. *

10. * @param  string $lang

11. * @return \Illuminate\Http\Response

12. */

13. public function __invoke($lang)

14. {

15. $language = in_array($lang, config('app.languages')) ? $lang : config('app.fallback_locale');

17. session()->set('locale', $language);

19. return back();

20. }

21. }

Ici on mémorise en session la nouvelle langue

Remarquez l'utilisation de la méthode __invoque pour un contrôleur qui ne possède qu'une méthode.

III-D - Les dates

III-D-1 - Le presenter

Il ne vous a sans doute pas échappé que les dates ne sont pas présentées de la même manière selon les langues utilisées. 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.

Il va donc être nécessaire de faire quelque chose pour que, par exemple, les dates de création des articles apparaissent au bon format dans chaque cas.

Comme ce formatage doit être appliqué pour toutes les dates, la façon la plus simple et la plus élégante de procéder est de créer un « accessor » sur la propriété. Ainsi chaque fois que l'on va extraire une date à partir du modèle, on va le formater au passage. On pourrait faire ceci directement dans notre modèle Post. Mais on va généraliser la chose, parce que l'on va aussi en avoir besoin dans d'autres modèles.

Dans ce genre de situation la création d'un trait est pertinente pour introduire une fonctionnalité sans passer par l'héritage.

On va utiliser une version simplifiée du Design pattern Décorateur (decorator). Dans Laravel on va appeler cela un « Model Presenter ».

On va créer un dossier pour les presenters et créer celui dont nous avons besoin :

Avec ce code :

1. <?php

3. namespace App\Presenters;

5. use Carbon\Carbon;

7. trait DatePresenter

8. {

9. /**

10. * Format created_at attribute

11. *

12. * @param \Carbon\Carbon  $date

13. * @return string

14. */

15. public function getCreatedAtAttribute($date)

16. {

17. return $this->getDateTimeFormated($date);

18. }

20. /**

21. * Format updated_at attribute

22. *

23. * @param \Carbon\Carbon  $date

24. * @return string

25. */

26. public function getUpdatedAtAttribute($date)

27. {

28. return $this->getDateTimeFormated($date);

29. }

31. /**

32. * Format date

33. *

34. * @param \Carbon\Carbon  $date

35. * @return string

36. */

37. protected function getDateTimeFormated($date)

38. {

39. return Carbon::parse($date)->format(config('app.locale') != 'en' ? 'd/m/Y H:i:s' : 'm/d/Y H:i:s');

40. }

41. }

On voit que selon la valeur de la locale on va appliquer le formatage adapté à la date extraite.

Remarquez que l'on utilise la date et aussi l'heure, selon les vues on pourra avoir besoin seulement de la date, sans l'heure, il faudra alors en tenir compte.

Il ne nous reste plus qu'à utiliser ce trait dans les modèles concernés, par exemple Post :

1. <?php

3. namespace App\Models;

5. use Illuminate\Database\Eloquent\Model;

6. use App\Presenters\DatePresenter;

7. ...

9. class Post extends Model

10. {

11. use DatePresenter;

III-E - Les vues

Une fois que tout ça est en place l'utilisation dans les vues est simple. Prenons, par exemple la vue pour la gestion des articles :

Le contenu du tableau est généré par la vue resources/views/back/blog/table.blade.php.

Les boutons doivent avoir un texte qui correspond à la langue. Par exemple, pour le bouton d'édition on a :

<td>{!! link_to_route('blog.edit', trans('back/blog.edit'), [$post->id], ['class' => 'btn btn-warning btn-block']) !!}</td>

On utilise l'helper trans pour traduite le titre du bouton qui sera dans la langue actuelle de l'utilisateur.

D'autre part la date doit avoir le bon format, ici elle est au format anglais, voici le code :

<td>{{ $post->created_at }}</td>

Vous voyez que l'on a rien de particulier à faire puisque tout se passe dans le modèle avec le trait mis en place.

Si on change la langue pour le français les textes et les dates s'adaptent automatiquement :

III-F - 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 trouvez ce tableau :

1. 'attributes' => [

2. "name" => "Nom",

3. "username" => "Pseudo",

4. ...

5. ],

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.

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.

III-G - En résumé

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

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

- La localisation peut s'effectuer au niveau d'un middleware.

- Le formatage des dates peut s'effectuer facilement avec un « presenter ».

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

IV - Les événements

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.

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

IV-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 que l'on crée la première classe avec Artisan qui dispose de commandes pour le faire :

IV-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 existants déjà dans le framework qui en propose au niveau de l'authentification :

- tentative de connexion (login) ;

- connexion (login) réussie ;

- déconnexion (logout) réussie.

On trouve aussi des événements avec Eloquent :

- creating ;

- created ;

- updating ;

- updated ;

- saving ;

- saved ;

- deleting ;

- deleted ;

- restoring, restored.

Pour tous ces cas, vous n'avez donc pas à vous inquiéter de l'événement et du déclencheur qui existent déjà, il faudra juste mettre en place une écoute (Listener).

IV-B-2 - Le fournisseur de service (service provider)

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

Dans l'application d'exemple 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. 'Illuminate\Auth\Events\Login' => ['App\Listeners\LoginSuccess'],

17. 'Illuminate\Auth\Events\Logout' => ['App\Listeners\LogoutSuccess'],

18. 'App\Events\UserAccess' => ['App\Listeners\UserAccess']

19. ];

21. /**

22. * Register any events for your application.

23. *

24. * @return void

25. */

26. public function boot()

27. {

28. parent::boot();

29. }

30. }

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.

On veut savoir quand quelqu'un se connecte ou se déconnecte. On prévoit donc des observateurs dans le provider :

1. protected $listen = [

2. 'Illuminate\Auth\Events\Login' => ['App\Listeners\LoginSuccess'],

3. 'Illuminate\Auth\Events\Logout' => ['App\Listeners\LogoutSuccess'],

4. ...

5. ];

Vous pouvez trouver ces événements dans la documentation.

Laravel prévoit de placer les observateurs (listeners) dans ce dossier en tant que classes :

IV-C - Le statut de l'utilisateur dans l'application

Pour simplifier la gestion le statut de l'utilisateur (son rôle) est enregistré dans la session. On a trois rôles :

- Administrateur (admin) : avec tous les droits ;

- Rédacteur (redac) : avec le droit de gérer des articles ;

- Utilisateur (user) : avec juste le droit de laisser des commentaires.

Et on ajoute visitor pour un utilisateur non connecté.

IV-C-1 - La connexion

Donc dès que quelqu'un se connecte on doit regarder son rôle et le mémoriser dans la session.

Si on regarde l'observateur (listener) :

1. <?php

3. namespace App\Listeners;

5. use Illuminate\Auth\Events\Login;

6. use App\Services\Statut;

8. class LoginSuccess

9. {

10. /**

11. * Handle the event.

12. *

13. * @param  Login  $login

14. * @return void

15. */

16. public function handle(Login $login)

17. {

18. Statut::setLoginStatut($login);

19. }

20. }

On fait appel à un service avec une méthode statique :

1. <?php

3. namespace App\Services;

5. class Statut

6. {

7. /**

8. * Set the login user statut

9. *

10. * @param  Illuminate\Auth\Events\Login $login

11. * @return void

12. */

13. public static function setLoginStatut($login)

14. {

15. session(['statut' => $login->user->getStatut()]);

16. }

18. ...

19. }

Il faut aller voir dans le modèle App\Models\User pour trouver la méthode :

1. public function getStatut()

2. {

3. return $this->role->slug;

4. }

On passe par la relation entre les utilisateurs et les rôles pour aller chercher l'information à mettre dans la session.

IV-C-2 - La deconnexion

Et évidemment en cas de déconnexion c'est le même processus avec le listener LogoutSuccess :

1. <?php

3. namespace App\Listeners;

5. use Illuminate\Auth\Events\Logout;

6. use App\Services\Statut;

8. class LogoutSuccess

9. {

10. /**

11. * Handle the event.

12. *

13. * @param  Logout  $event

14. * @return void

15. */

16. public function handle(Logout $event)

17. {

18. Statut::setVisitorStatut();

19. }

20. }

On utilise encore le service :

1. public static function setVisitorStatut()

2. {

3. session(['statut' => 'visitor']);

4. }

IV-C-3 - Accès à une page

Il faut considérer un dernier cas : lorsque l'utilisateur accède à une page on va vérifier s'il y a un statut mémorisé en session et si ce n'est pas le cas, en créer un.

Cette fois ce n'est pas un événement qui existe comme login ou logout, il nous faut donc le créer.

On a donc cette fois une classe Event :

Avec ce code :

1. <?php

3. namespace App\Events;

5. class UserAccess extends Event

6. {

7. //

8. }

Oui il n'y a que la classe parce que l'on a rien de particulier à transmettre dans ce cas.

On doit donc déclencher cet événement quand un utilisateur accède à une page.

On va donc passer par un middleware. Regardez dans la classe App\Http\Middleware\App :

1. public function handle(Request $request, Closure $next)

2. {

3. event(new UserAccess);

5. return $next($request);

6. }

On utilise l'helper event pour déclencher l'événement. Sans l'helper il faudrait utiliser la façade :

Event::fire(new UserAccess);

Au niveau de l'inscription on a vu que l'on a ceci :

1. protected $listen = [

2. ...

3. 'App\Event\UserAccess' => ['App\Listeners\UserAccess']

4. ];

Voici le code du listener :

1. <?php

3. namespace App\Listeners;

5. use App\Events\UserAccess as UserAccessEvent;

6. use App\Services\Statut;

7. use App\Services\Locale;

9. class UserAccess

10. {

11. /**

12. * Handle the event.

13. *

14. * @param  UserAccess  $event

15. * @return void

16. */

17. public function handle(UserAccessEvent $event)

18. {

19. Statut::setStatut();

21. Locale::setLocale();

22. }

23. }

On appelle encore la méthode dans le service :

1. public static function setStatut()

2. {

3. if (!session()->has('statut')) {

4. session(['statut' =>  auth()->check() ?  auth()->user()->getStatut() : 'visitor']);

5. }

6. }

On a la logique ici : si on n'a pas de statut dans la session on regarde si l'utilisateur est connecté, si c'est le cas on va chercher son statut, sinon on lui affecte le statut visitor.

Remarquez aussi dans le listener la gestion de la locale que l'on a vue dans un précédent chapitre.

Pour avoir la documentation complète sur ce sujet c'est dans la documentation.

La programmation événementielle en PHP est un certain style de programmation. Elle n'a rien d'obligatoire parce que l'on peut toujours s'en passer si on préfère coder de façon plus classique. Mais il y a de nombreuses situations où elle présente un intérêt certain, en particulier quand un événement possède plusieurs observateurs indépendants les uns des autres. D'autre part Laravel offre des événements précodés, comme on l'a vu dans ce chapitre, qu'il serait dommage de ne pas utiliser.

IV-D - En résumé

- Laravel comporte un système complet et simple de gestion des événements.

- Laravel comporte déjà des événements pour l'authentification et la gestion des données avec Eloquent.

- On doit créer une classe Event et une classe Listener pour mettre en place une gestion événementielle et ensuite définir un déclencheur.

V - La sécurité

Lorsque l'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, par exemple savoir si un utilisateur authentifié a le droit de modifier une ressource particulière. Laravel nous offre un système d'autorisations bien pratique.

V-A - L'authentification

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. Ce statut est conservé en session comme on l'a vu dans le précédent chapitre sur les événements.

C'est bien pratique, par exemple pour adapter les menus :

1. @if(session('statut') == 'visitor')

2. <li {!! classActivePath('login') !!}>

3. {!! link_to('login', trans('front/site.connection')) !!}

4. </li>

5. @else

6. @if(session('statut') == 'admin')

7. <li>

8. {!! link_to_route('admin', trans('front/site.administration')) !!}

9. </li>

10. @elseif(session('statut') == 'redac')

11. <li>

12. {!! link_to('blog', trans('front/site.redaction')) !!}

13. </li>

14. @endif

15. <li>

16. {!! link_to('/logout', trans('front/site.logout'), ['id' => "logout"]) !!}

17. {!! Form::open(['url' => '/logout', 'id' => 'logout-form']) !!}{!! Form::close() !!}

18. </li>

19. @endif

Là il ne s'agit évidemment pas de sécurité mais de commodité.

Par contre on peut filtrer les accès selon le statut avec des middlewares. Dans l'application d'exemple on a le middleware isAdmin :

Avec ce code :

1. <?php

3. namespace App\Http\Middleware;

5. use Closure;

7. class IsAdmin

8. {

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

10. {

11. if (session('statut') === 'admin') {

12. return $next($request);

13. }

15. return redirect('/');

16. }

17. }

Il suffit ensuite de l'utiliser dans les contrôleurs, par exemple ici dans AdminController :

1. public function __construct()

2. {

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

4. }

Ainsi toutes les méthodes du contrôleur sont réservées aux administrateurs.

On a de la même manière le middleware isRedactor.

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é. En cas d'un certain nombre de connexions avortées (cinq par défaut) on bloque l'accès pour le couple « email (ou autre)/mot de passe » pendant une minute.

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

V-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 (des explications plus précises ici).

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.

V-B-2 - CSRF

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

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

V-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 (celle que vous mettez dans la propriété $fillable d'un modèle).

V-C - Les requêtes de formulaire et le pot de miel

Un autre lieu où on peut sécuriser l'application est 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 est située uniquement dans la classe Request qui est la base de toutes les autres :

1. <?php

3. namespace App\Http\Requests;

5. use Illuminate\Foundation\Http\FormRequest;

7. abstract class Request extends FormRequest

8. {

9. /**

10. * Check honeypot

11. *

12. * @return boolean

13. */

14. public function authorize()

15. {

16. return  $this->input('address') == '';

17. }

18. }

En effet dans tous les formulaires il est prévu un contrôle caché. Par exemple, dans le formulaire de contact on trouve ce code :

{!! Form::text('address', '', ['class' => 'hpet']) !!}

La classe hpet se contente de rendre le contrôle invisible. Un humain ne voit pas ce contrôle et donc ne le complète pas, par contre un robot risque de l'utiliser.

C'est ce que l'on appelle un pot de miel (pour attirer les abeilles virtuelles).

On vérifie donc si ce contrôle est rempli et, si c'est le cas, on bloque le processus.

C'est évidemment une protection sommaire mais assez efficace. Une autre approche plus classique est d'utiliser un captcha. Il existe de nombreux composant adaptés pour Laravel.

V-D - Les autorisations

En plus de ces possibilités Laravel nous offre un système complet d'autorisations. 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 un fichier :

Voyons ce fichier :

1. <?php

3. namespace App\Policies;

5. use App\Models\Post;

6. use App\Models\User;

8. class PostPolicy

9. {

10. /**

11. * Grant all abilities to administrator.

12. *

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

14. * @param  string  $ability

15. * @return bool

16. */

17. public function before(User $user, $ability)

18. {

19. if (session('statut') == 'admin') {

20. return true;

21. }

22. }

24. /**

25. * Determine if the given post can be changed by the user.

26. *

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

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

29. * @return bool

30. */

31. public function change(User $user, Post $post)

32. {

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

34. }

35. }

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

On a ensuite la méthode change avec comme paramètres l'utilisateur et l'article. Si l'utilisateur est le créateur de l'article on renvoie true.

Maintenant que ces autorisations sont en place il faut les déclarer. Regardez le fichier app/Providers/AuthServiceProvider :

1. <?php

3. namespace App\Providers;

5. use Illuminate\Support\Facades\Gate;

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

7. use App\Models\Post;

8. use App\Policies\PostPolicy;

10. class AuthServiceProvider extends ServiceProvider

11. {

12. /**

13. * The policy mappings for the application.

14. *

15. * @var array

16. */

17. protected $policies = [

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

19. ];

21. /**

22. * Register any authentication / authorization services.

23. *

24. * @return void

25. */

26. public function boot(Gate $gate)

27. {

28. $this->registerPolicies($gate);

29. }

30. }

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\Foundation\Bus\DispatchesJobs;

6. use Illuminate\Routing\Controller as BaseController;

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

1. public function edit($id)

2. {

3. $post = $this->blogRepository->getByIdWithTags($id);

5. $this->authorize('change', $post);

7. return view('back.blog.edit', $this->blogRepository->getPostWithTags($post));

8. }

10. public function update(PostRequest $request, $id)

11. {

12. $post = $this->blogRepository->getById($id);

14. $this->authorize('change', $post);

16. $this->blogRepository->update($request->all(), $post);

18. return redirect('blog')->with('ok', trans('back/blog.updated'));

19. }

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('change', $post);

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

‌Vous pouvez trouver la documentation complète ici.

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

VI - Les notifications

On a vu dans ce cours comment envoyer un email avec Laravel. Mais on dispose aussi d'un système complet de notifications, par exemple par SMS, qui inclut aussi les emails 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 email 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 pour l'envoi d'emails pour :

- la confirmation de l'adresse email lors de l'enregistrement ;

- 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 à leurs articles.

VI-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é lorsque l'on crée la première notification avec Artisan qui dispose à cet effet d'une commande :

VI-B - La confirmation de l'adresse email

Pour voir comment fonctionnent les notifications on va voir en détail la confirmation de l'adresse email de l'application d'exemple.

Par défaut cette confirmation n'est pas prévue dans Laravel. Du coup lorsque quelqu'un s'enregistre en indiquant une adresse email on n'est pas sûr qu'elle soit valide.

VI-B-1 - Le contrôleur RegisterController

Par défaut le contrôleur App\Http\Controllers\Auth\RegisterController contient ce code :

1. <?php

3. namespace App\Http\Controllers\Auth;

5. use App\User;

6. use Validator;

7. use App\Http\Controllers\Controller;

8. use Illuminate\Foundation\Auth\RegistersUsers;

10. class RegisterController extends Controller

11. {

12. /*

13. |--------------------------------------------------------------------------

14. | Register Controller

15. |--------------------------------------------------------------------------

16. |

17. | This controller handles the registration of new users as well as their

18. | validation and creation. By default this controller uses a trait to

19. | provide this functionality without requiring any additional code.

20. |

21. */

23. use RegistersUsers;

25. /**

26. * Where to redirect users after login / registration.

27. *

28. * @var string

29. */

30. protected $redirectTo = '/home';

32. /**

33. * Create a new controller instance.

34. *

35. * @return void

36. */

37. public function __construct()

38. {

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

40. }

42. /**

43. * Get a validator for an incoming registration request.

44. *

45. * @param  array  $data

46. * @return \Illuminate\Contracts\Validation\Validator

47. */

48. protected function validator(array $data)

49. {

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

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

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

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

54. ]);

55. }

57. /**

58. * Create a new user instance after a valid registration.

59. *

60. * @param  array  $data

61. * @return User

62. */

63. protected function create(array $data)

64. {

65. return User::create([

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

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

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

69. ]);

70. }

71. }

Autrement dit toute la logique pour l'enregistrement se fait dans le framework, dans le trait lluminate\Foundation\Auth\RegistersUsers. Pour modifier cette logique on doit surcharger la méthode correspondante :

1. public function register(Request $request)

2. {

3. $this->validator($request->all())->validate();

5. $this->guard()->login($this->create($request->all()));

7. return redirect($this->redirectPath());

8. }

Ici on trouve :

- la validation ;

- la création de l'utilisateur et sa connexion ;

- une redirection.

On veut ajouter la validation de l'adresse email et évidemment supprimer la connexion automatique. Voici la méthode qui surcharge celle du trait :

1. public function register(RegisterRequest $request, UserRepository $userRepository)

2. {

3. $user = $userRepository->store(

4. $request->all(),

5. str_random(30)

6. );

8. $this->notifyUser($user);

10. return redirect('/')->with('ok', trans('front/verify.message'));

11. }

On voit que :

- la validation se fait maintenant avec une requête de formulaire (ce n'était pas indispensable mais homogène par rapport à toutes les validations de l'application) ;

- on envoie une notification (notify) créée par la classe ConfirmEmail ;

- on redirige sur la page d'accueil du site avec un message localisé.

Remarquez que l'on ajoute aux données du formulaire d'enregistrement un code de confirmation (une chaîne aléatoire de 30 caractères).

La partie qui nous intéresse est celle de la notification. Pour que ça fonctionne le modèle doit utiliser le trait Notifiable, ce qui est par défaut le cas du modèle User :

class User extends Authenticatable
{
use Notifiable;

VI-B-2 - La notification

Voici la classe App\Notifications\ConfirmEmail :

1. <?php

3. namespace App\Notifications;

5. use Illuminate\Bus\Queueable;

6. use Illuminate\Notifications\Notification;

7. use Illuminate\Contracts\Queue\ShouldQueue;

8. use Illuminate\Notifications\Messages\MailMessage;

10. class ConfirmEmail extends Notification implements ShouldQueue

11. {

12. use Queueable;

14. /**

15. * The password reset token.

16. *

17. * @var string

18. */

19. public $confirmation_code;

21. /**

22. * Create a notification instance.

23. *

24. * @param  string  $confirmation_code

25. * @return void

26. */

27. public function __construct($confirmation_code)

28. {

29. $this->confirmation_code = $confirmation_code;

30. }

32. /**

33. * Get the notification's delivery channels.

34. *

35. * @param  mixed  $notifiable

36. * @return array

37. */

38. public function via($notifiable)

39. {

40. return ['mail'];

41. }

43. /**

44. * Get the mail representation of the notification.

45. *

46. * @param  mixed  $notifiable

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

48. */

49. public function toMail($notifiable)

50. {

51. return (new MailMessage)

52. ->subject(trans('front/verify.email-title'))

53. ->line(trans('front/verify.email-title'))

54. ->line(trans('front/verify.email-intro'))

55. ->action(trans('front/verify.email-button'), url('confirm/' . $this->confirmation_code));

56. }

57. }

On définit le canal de notification (via), ici mail (ça pourrait être database, broadcast, nexmo ou slack).

La méthode toMail définit le contenu de l'email. Il existe un lot de méthodes pratiques :

- subject : pour le sujet de l'email ;

- line : pour ajouter une ligne de texte :

- action : pour ajouter un bouton de lien ;

- …

Laravel est équipé d'un système de file d'attente (queue) qui permet de différer les tâches consommatrices de ressources, comme l'envoi d'un email. Ici c'est mis en œuvre avec l'interface ShouldQueue et le trait Queueable). Je ne développe pas cet aspect dans ce cours. Vous pouvez trouver la documentation ici.

VI-B-3 - L'email

Voici l'email résultant en français :

Vous avez une mise en forme automatique !

Par défaut le template est dans le framework. Mais si on doit le modifier, comme c'est le cas pour l'application d'exemple pour la localisation, il existe une commande d'Artisan pour le publier :

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

On trouve le template ici :

Il devient ainsi accessible et modifiable.

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

VI-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 cette table notifications :

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

VI-C-2 - La notification

Comme pour les emails 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\User\Post

14. */

15. protected $post;

17. /**

18. * Create a new notification instance.

19. *

20. * @param \App\Models\Post

21. * @return void

22. */

23. public function __construct(Post $post)

24. {

25. $this->post = $post;

26. }

28. /**

29. * Get the notification's delivery channels.

30. *

31. * @param  mixed  $notifiable

32. * @return array

33. */

34. public function via($notifiable)

35. {

36. return ['database'];

37. }

39. /**

40. * Get the array representation of the notification.

41. *

42. * @param  mixed  $notifiable

43. * @return array

44. */

45. public function toArray($notifiable)

46. {

47. return [

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

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

50. ];

51. }

52. }

Cette fois dans la méthode via on a database.

On voit que l'on doit transmettre dans le constructeur une instance de l'article concerné ($post). 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 et le slug (pour générer l'URL) de l'article.

VI-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\CommentController) :

1. public function store(CommentRequest $request, BlogRepository $blogRepository)

2. {

3. $this->commentRepository->store($request->all(), $request->user()->id);

5. $blog = $blogRepository->getById($request->post_id);

7. $blog->user->notify(new Commented($blog));

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

10. $request->session()->flash('warning', trans('front/blog.warning'));

11. }

13. return back();

14. }

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

$blog->user->notify(new Commented($blog));

Exactement comme on l'a vu pour les email 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 et slug).

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

On a deux routes :

Route::get('notifications/{user}', 'NotificationController@index');
Route::put('notifications/{notification}', 'NotificationController@update');

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

1. <?php

3. namespace App\Http\Controllers;

5. use Illuminate\Http\Request;

6. use App\Models\User;

7. use Illuminate\Notifications\DatabaseNotification;

9. class NotificationController extends Controller

10. {

11. /**

12. * Create a new NotificationController instance.

13. *

14. * @return void

15. */

16. public function __construct()

17. {

18. $this->middleware('redac');

19. }

21. /**

22. * Display a listing notifications.

23. *

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

25. * @return \Illuminate\Http\Response

26. */

27. public function index(User $user)

28. {

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

30. }

32. /**

33. * Update the notification.

34. *

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

36. * @param  integer $id

37. * @return \Illuminate\Http\Response

38. */

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

40. {

41. $notification->markAsRead();

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

44. return redirect()->route('blog.index');

45. }

47. return back();

48. }

49. }

On applique le middleware redac pour que les méthodes ne soient accessibles qu'aux rédacteurs.

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.

VI-C-5 - La vue index

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

1. @extends('back.template')

3. @section('main')

5. @include('back.partials.entete', ['title' => trans('back/notifications.notifications'), 'icon' => 'bell', 'fil' => trans('back/notifications.new-notifications')])

7. <div class="row col-lg-12">

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

9. <table class="table">

10. <thead>

11. <tr>

12. <th>{!! trans('back/notifications.post') !!}</th>

13. <th>{!! trans('back/notifications.date') !!}</th>

14. <th>{!! trans('back/notifications.valid') !!}</th>

15. <th></th>

16. </tr>

17. </thead>

18. <tbody>

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

20. <tr>

21. <td>{!! link_to('blog/' . $notification->data['slug'], $notification->data['title']) !!}</td>

22. <td>{{ formatDate($notification->created_at) }}</td>

23. <td>{!! Form::checkbox(trans('valid'), null, $user->valid, ['disabled' => true]) !!}</td>

24. <td>

25. {!! Form::open(['method' => 'PUT', 'url' => 'notifications/' . $notification->id]) !!}

26. {!! Form::submit(trans('back/notifications.erase'), ['class' => 'btn btn-success btn-block']) !!}

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

28. </td>

29. </tr>

30. @endforeach

31. </tbody>

32. </table>

33. </div>

34. </div>

36. @endsection

On voit que l'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 « Effacer » 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) et 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.

VI-D - En résumé

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

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

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

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

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

Pour se connecter en tant qu'administrateur il suffit d'utiliser le login admin@la.fr avec le mot de passe admin (si vous avez effectué la population prévue dans l'application).

Les messages qui n'ont pas encore été vus apparaissent avec un fond jauni, par défaut on en a deux. D'autre part l'administrateur dispose pour chaque message d'une case à cocher pour changer ce statut.

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 et localement de changer la couleur du fond.

Pour améliorer l'ergonomie on a aussi prévu une petite animation à la place de la case à cocher le temps de l'échange de requêtes :

Il faut aussi gérer cette animation avec JavaScript.

VII-B - La vue des messages

Dans la vue des messages (views/back/messages/index.blade.php) on a cette partie du code chargée de l'affichage du tableau des messages :

1. @foreach ($messages as $message)

2. <div class="panel {!! $message->seen? 'panel-default' : 'panel-warning' !!}">

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

4. <table class="table">

5. <thead>

6. <tr>

7. <th class="col-lg-1">{{ trans('back/messages.name') }}</th>

8. <th class="col-lg-1">{{ trans('back/messages.email') }}</th>

9. <th class="col-lg-1">{{ trans('back/messages.date') }}</th>

10. <th class="col-lg-1">{{ trans('back/messages.seen') }}</th>

11. <th class="col-lg-1"></th>

12. </tr>

13. </thead>

14. <tbody>

15. <tr>

16. <td class="text-primary"><strong>{{ $message->name }}</strong></td>

17. <td>{!! HTML::mailto($message->email, $message->email) !!}</td>

18. <td>{{ $message->created_at }}</td>

19. <td>{!! Form::checkbox('seen', $message->id, $message->seen) !!}</td>

20. <td>

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

22. {!! Form::destroyBootstrap(trans('back/messages.destroy'), trans('back/messages.destroy-warning'), 'btn-xs') !!}

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

24. </td>

25. </tr>

26. </tbody>

27. </table>

28. </div>

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

30. {{ $message->message }}

31. </div>

32. </div>

33. @endforeach

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

@foreach ($messages as $message)
...
@endforeach

On gère la couleur de fond avec deux classes de Bootstrap :

<div class="panel {!! $message->seen? 'panel-default' : 'panel-warning' !!}">

Les cases à cocher sont générées classiquement (laravelcollective/html) :

<td>{!! Form::checkbox('seen', $message->id, $message->seen) !!}</td>

Ce qui donne ce code pour la case cochée :

<td><input checked="checked" name="seen" type="checkbox" value="3"></td>

On distingue les cases avec l'attribut value dans lequel on mémorise l'identifiant du message.

VII-C - Le JavaScript

On a aussi sur la page le JavaScript pour gérer Ajax :

1. $(function () {

2. $(':checkbox').change(function () {

3. $(this).parents('.panel').toggleClass('panel-warning').toggleClass('panel-default');

4. $(this).hide().parent().append('<i class="fa fa-refresh fa-spin"></i>');

5. $.ajax({

6. url: 'contact/' + this.value,

7. type: 'PUT',

8. data: 'seen=' + this.checked

9. })

10. .done(function () {

11. $('.fa-spin').remove();

12. $('input[type="checkbox"]:hidden').show();

13. })

14. .fail(function () {

15. $('.fa-spin').remove();

16. var chk = $('input[type="checkbox"]:hidden');

17. chk.parents('.panel').toggleClass('panel-warning').toggleClass('panel-default');

18. chk.show().prop('checked', chk.is(':checked') ? null : 'checked');

19. alert('{{ trans("back/messages.fail") }}');

20. });

21. });

22. });

On utilise jQuery qui est déjà chargé pour toutes les pages.

Même si ce n'est pas l'objet de ce cours on va un peu analyser ce code…

On détecte les changements sur toutes les cases à cocher (on n'a pas d'autres cases à cocher que celles des messages sur la page) :

$(':checkbox').change(function () {

On change la couleur de fond :

$(this).parents('.panel').toggleClass('panel-warning').toggleClass('panel-default');

On cache la case à cocher et on fait apparaître l'icône animée :

$(this).hide().parent().append('<i class="fa fa-refresh fa-spin"></i>');

On envoie la requête Ajax :

1. $.ajax({

2. url: 'contact/' + this.value,

3. type: 'PUT',

4. data: 'seen=' + this.checked

5. })

On définit ainsi :

- l'URL : de la forme contact/id ;

- la méthode : PUT ;

- l'information : seen: true ou false. (une autre syntaxe possible est data: {seen:this.checked}).

Si tout fonctionne bien on exécute ce code :

1. .done(function () {

2. $('.fa-spin').remove();

3. $('input[type="checkbox"]:hidden').show();

4. })

On supprime la petite animation et on affiche à nouveau la case à cocher.

S'il y a eu un problème on exécute ce code :

1. .fail(function () {

2. $('.fa-spin').remove();

3. var chk = $('input[type="checkbox"]:hidden');

4. chk.parents('.panel').toggleClass('panel-warning').toggleClass('panel-default');

5. chk.show().prop('checked', chk.is(':checked') ? null : 'checked');

6. alert('{{ trans('back/messages.fail') }}');

7. });

- on supprime aussi la petite animation ;

- on change à nouveau la couleur de fond puisqu'on a échoué ;

- on fait réapparaître la case à cocher en changeant son état ;

- on affiche une alerte pour prévenir du problème.

VII-C-1 - 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 :

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 template (resources/views/back/template.blade.php) vous aller trouver ce code :

1. ...

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

3. ...

4. <script>

6. $(function() {

8. $.ajaxSetup({

9. headers: {

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

11. }

12. ...

13. }

14. </script>

15. ...

Le jeton (token) est mémorisé dans les metas et 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…

VII-D - Le traitement côté serveur

VII-D-1 - Le contrôleur

Voici la partie concernée du contrôleur de ressource :

1. <?php

3. namespace App\Http\Controllers;

5. use Illuminate\Http\Request;

6. use App\Http\Requests\ContactRequest;

7. use App\Repositories\ContactRepository;

9. class ContactController extends Controller

10. {

11. /**

12. * The ContactRepository instance.

13. *

14. * @var \App\Repositories\ContactRepository

15. */

16. protected $contactRepository;

18. /**

19. * Create a new ContactController instance.

20. *

21. * @param  \App\Repositories\ContactRepository $contactRepository

22. * @return void

23. */

24. public function __construct(ContactRepository $contactRepository)

25. {

26. $this->contactRepository = $contactRepository;

28. $this->middleware('admin')->except('create', 'store');

29. $this->middleware('ajax')->only('update');

30. }

32. ...

34. /**

35. * Update the specified contact in storage.

36. *

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

38. * @param  int  $id

39. * @return \Illuminate\Http\Response

40. */

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

42. {

43. $this->contactRepository->update($request->input('seen'), $id);

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

46. }

48. ...

49. }

La requête arrive dans la méthode update. On fait appel au repository pour modifier la base :

1. public function update($seen, $id)

2. {

3. $contact = $this->getById($id);

5. $contact->seen = $seen == 'true';

7. $contact->save();

8. }

Et on renvoie une réponse JSON.

VII-D-2 - Le middleware

‌On peut remarquer que la méthode concernée (update) est protégée par un middleware ajax.

En effet on veut que notre méthode ne soit accessible que si on reçoit une requête Ajax. Ce middleware n'existe pas par défaut dans Laravel, il faut donc le créer (app/Http/Middlewares/Ajax.php) :

1. <?php

3. namespace App\Http\Middleware;

5. use Closure;

7. class IsAjax

8. {

9. /**

10. * Handle an incoming request.

11. *

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

13. * @param  \Closure  $next

14. * @return mixed

15. */

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

17. {

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

19. return $next($request);

20. }

22. abort(404);

23. }

24. }

La méthode ajax de la requête nous permet facilement de savoir s'il s'agit d'une requête de ce type, si ce n'est pas le cas on revoie une erreur 404.

Il faut informer Laravel que notre middleware existe (app/Http/Kernel.php) :

1. protected $routeMiddleware = [

2. ...

3. 'ajax' => \App\Http\Middleware\IsAjax::class

4. ];

VII-E - 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.

- On peut prévoir un middleware particulier pour les requêtes Ajax.

VIII - Les tests unitaires

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.

Lorsque l'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.

VIII-A - L'intendance des tests

VIII-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. "fzaninotto/faker": "~1.4",

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

4. "phpunit/phpunit": "~5.0",

5. "symfony/css-selector": "3.1.*",

6. "symfony/dom-crawler": "3.1.*"

7. },

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

La version 5 de PHPUnit nécessite au minimum PHP 5.6

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.

VIII-A-2 - 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 fichiers. Voilà le code de TestCase.php :

1. <?php

3. abstract class TestCase extends Illuminate\Foundation\Testing\TestCase

4. {

5. /**

6. * The base URL to use while testing the application.

7. *

8. * @var string

9. */

10. protected $baseUrl = 'http://localhost';

12. /**

13. * Creates the application.

14. *

15. * @return \Illuminate\Foundation\Application

16. */

17. public function createApplication()

18. {

19. $app = require __DIR__.'/../bootstrap/app.php';

21. $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

23. return $app;

24. }

25. }

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.

Il y a un exemple de test déjà présent ExampleTest.php :

1. <?php

3. use Illuminate\Foundation\Testing\WithoutMiddleware;

4. use Illuminate\Foundation\Testing\DatabaseMigrations;

5. use Illuminate\Foundation\Testing\DatabaseTransactions;

7. class ExampleTest extends TestCase

8. {

9. /**

10. * A basic functional test example.

11. *

12. * @return void

13. */

14. public function testBasicExample()

15. {

16. $this->visit('/')

17. ->see('Laravel');

18. }

19. }

Sans entrer pour le moment dans le code sachez simplement que l'on envoie une requête pour la route de base et que l'on attend une réponse. Pour lancer ce test c'est très simple, entrez cette commande :

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

VIII-A-3 - 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="bootstrap/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="Application Test Suite">

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

14. </testsuite>

15. </testsuites>

16. <filter>

17. <whitelist processUncoveredFilesFromWhitelist="true">

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

19. </whitelist>

20. </filter>

21. <php>

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

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

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

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

26. </php>

27. </phpunit>

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

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

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

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

- QUEUE_DRIVER : je n'évoque pas les tâches dans ce cours, on ne va donc pas tenir compte de cette variable.

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 config/database.php :

'default' => env('DB_CONNECTION', 'mysql'),

Il faut donc la renseigner dans .env :

DB_CONNECTION=mysql

Du coup dans phpunit.xml je peux écrire :

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

Maintenant pour les tests je vais utiliser sqlite.

VIII-B - Construire un test

VIII-B-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. Remplaçons le code de la méthode testBasicExample par celui-ci :

1. public function testBasicExample()

2. {

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

4. $result = array_sum($data);

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

6. }

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 que l'on peut obtenir comme renseignement en cas d'échec.

VIII-C - Assertions et appel de routes

VIII-C-1 - Les assertions

Les assertions constituent l'outil de base des tests. On en a vu une ci-dessus et il en existe bien d'autres. Vous pouvez en trouver la liste complète ici.

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

1. public function testBasicExample()

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.

VIII-C-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 :

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

2. {

3. return 'coucou';

4. });

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

1. public function testBasicExample()

2. {

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

4. $this->assertResponseOk();

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

6. }

L'assertion assertResponseOk nous assure que la réponse est correcte. Ce n'est pas une assertion de PHPUnit mais une spécifique de Laravel. La méthode getContent permet de lire la réponse. On obtient :

VIII-D - Les vues et les contrôleurs

VIII-D-1 - Les vues

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

Mettez ce code pour la route :

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

2. {

3. return view('welcome')->with('message', 'Vous y êtes !');

4. });

Ajoutez dans cette vue ceci :

{{ $message }}

Maintenant voici le test :

1. public function testBasicExample()

2. {

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

4. $view = $response->original;

5. $this->assertEquals('Vous y êtes !', $view['message']);

6. }

On envoie la requête et on récupère la réponse. Ensuite la méthode original nous permet de récupérer la vue. On peut alors tester la valeur de la variable $message dans la vue.

Laravel propose une autre assertion pour effectuer presque la même chose :

1. public function testBasicExample()

2. {

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

4. $this->assertViewHas('message');

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

6. }

L'assertion assertViewHas permet de vérifier qu'une vue reçoit bien une donnée, on peut aussi grâce au second paramètre vérifier sa valeur.

VIII-D-2 - Les contrôleurs

Créez ce contrôleur :

1. <?php

3. namespace App\Http\Controllers;

5. class WelcomeController extends Controller

6. {

7. public function index()

8. {

9. return view('welcome');

10. }

11. }

Vous pouvez tester facilement les actions d'un contrôleur.

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 cette architecture de dossiers et le fichier WelcomeControllerTest.php :

Et mettez ce code dans le fichier WelcomeControllerTest.php :

1. <?php

3. class WelcomeControllerTest extends TestCase

4. {

5. public function testIndex()

6. {

7. $response = $this->action('GET', 'WelcomeController@index');

8. $this->assertResponseOk();

9. }

10. }

Il y a une commande d'artisan pour créer une classe de test :

php artisan make:test WelcomeControllerTest

Si vous lancez le test ça devrait bien se passer :

Donc pour appeler une action d'un contrôleur il suffit d'utiliser la méthode action et de préciser le verbe, le nom du contrôleur et le nom de la méthode associée.

D'autre part il est important de bien organiser les tests pour s'y retrouver. Une bonne façon de procéder est d'adopter la même architecture de dossiers que celle prévue pour l'application.

VIII-E - 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 que l'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. ...

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

4. ...

5. },

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 :

VIII-E-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 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. $livres = $livre->all();

17. return view('welcome', compact('livres'));

18. }

19. }

J'ai prévu l'injection d'un modèle dans la méthode index. Ce n'est pas forcément très judicieux d'un point de vue conceptuel et il vaudrait mieux passer par un repository comme nous l'avons vu plusieurs fois dans ce cours mais on va prendre cette situation simple pour exécuter un test.

D'ailleurs la classe Livre n'a pas besoin de vraiment exister puisqu'on ne va pas vraiment l'utiliser.

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. use Illuminate\Database\Eloquent\Collection;

5. class WelcomeControllerTest extends TestCase

6. {

7. public function testIndex()

8. {

9. // Création de la collection

10. $collection = new Collection;

11. foreach (range(1, 2) as $i) {

12. $collection->add((object) ['titre' => 'Titre' . $i]);

13. }

15. // Création Mock

16. $mock = Mockery::mock('Livre');

17. $mock ->shouldReceive('all')

18. ->once()

19. ->andReturn($collection);

21. // Création lien

22. $this->app->instance('Livre', $mock);

24. // Action

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

27. // Assertions

28. $this->assertResponseOk();

29. $this->assertViewHas('livres');

31. }

33. public function tearDown()

34. {

35. Mockery::close();

36. }

37. }

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

@foreach ($livres as $livre)
<p>Titre : {{ $livre->titre }}</p>
@endforeach

Si je lance le test j'obtiens :

Un test et deux assertions correctes. Voyons de plus près ce code…

Au départ on crée une collection de livres :

1. // Création de la collection

2. $collection = new Collection;

3. foreach (range(1, 2) as $i) {

4. $collection->add((object) ['titre' => 'Titre' . $i]);

5. }

On crée ensuite un objet Mock en lui demandant de simuler la classe Livre :

$mock = Mockery::mock('Livre');

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

$mock ->shouldReceive('all')
->once()
->andReturn($collection);

On lui dit que s'il reçoit l'appel de la méthode « all » (shouldReceive) une fois (once) il doit retourner la collection $collection (andReturn).

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

$this->app->instance('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 fait l'action, ici la requête :

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

Pour finir on prévoit deux assertions, une pour vérifier que l'on a une réponse correcte et la seconde pour vérifier que l'on a bien les livres dans la vue :

$this->assertResponseOk();
$this->assertViewHas('livres');

Pour finir on prévoit, pour travailler proprement, de supprimer l'objet Mock à l'issue du test dans la méthode tearDown qui sert justement à effecteur des actions après un test :

1. public function tearDown()

2. {

3. Mockery::close();

4. }

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, quant à 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.

VIII-F - Tester une application

Laravel va encore plus loin dans la convivialité pour les tests en offrant la possibilité de tester facilement une application. On va le voir avec l'application d'exemple. Voici le dossier des tests :

Pour que les tests fonctionnent dans l'application d'exemple il faut d'abord rafraîchir les migrations :

php artisan migrate:reset
php artisan migrate --seed

Si vous lancez les tests ça va prendre un certain temps :

On va aller voir de plus près ces tests…

VIII-F-1 - Visite et clic sur un lien

On va commencer par quelque chose de simple avec HomeTest :

1. <?php

3. class HomeTest extends TestCase

4. {

5. /**

6. * Home page test

7. *

8. * @return void

9. */

10. public function testBasicExample()

11. {

12. $this->visit('/')

13. ->see('Laravel 5')

14. ->click('Home')

15. ->see('Laravel 5');

16. }

17. }

On a ces actions :

- on visite (visit) l'URL de base (‘/') ;

- on peut voir (see) « Laravel 5 » ;

- on clique (click) sur le lien « Home » ;

- on peut voir (see) « Laravel 5 ».

Vous voyez que l'on se met dans la peau d'un utilisateur devant l'application.

VIII-F-2 - Formulaire

On peut aussi remplir et soumettre un formulaire, regardez ce code dans ContactTest :

1. <?php

2. public function testOk()

3. {

4. $this->visit('/contact/create')

5. ->type('Someone', 'name')

6. ->type('someone@somewhere.com', 'email')

7. ->type('A message', 'message')

8. ->press('Send')

9. ->see('Your message has been recorded')

10. ->seeInDatabase('contacts', [

11. 'name' => 'Someone',

12. 'email' => 'someone@somewhere.com',

13. 'message' => 'A message'

14. ]);

15. }

On a ces actions :

- visite (visit) de l'URL /contact/create ;

- entrée de Someone dans le contrôle name ;

- entrée de someone@somewhere.com dans le contrôle email ;

- entrée de A message dans le contrôle message ;

- action sur le bouton Send ;

- on peut voir (see) « Your message has been recorded » ;

- on peut voir que l'on a dans la base (seeInDatabase) les informations transmises.

On peut aussi tester le fonctionnement de la validation, par exemple pour les required pour le même formulaire :

1. public function testRequired()

2. {

3. $this->visit('/contact/create')

4. ->press('Send')

5. ->see('The name field is required')

6. ->see('The email field is required')

7. ->see('The message field is required');

8. }

Vous voyez que tout ça se lit comme de la prose !

Vous pouvez aller voir le code des autres tests de l'application qui sont fondés sur le même principe.

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

IX - Le déploiement

Une application se développe et se teste en local mais arrive un moment où il faut la mettre sur un serveur pour qu'elle devienne visible et accessible. Ce déploiement n'est pas forcément une tâche aisée selon le contexte. Dans ce chapitre nous allons faire un petit tour d'horizon de ce qu'il convient de faire sans pouvoir être exhaustif étant donné la multiplicité des configurations existantes.

IX-A - L'environnement

IX-A-1 - Créer des environnements

Pour la gestion de l'environnement Laravel fait usage d'un package tiers : DotEnv. J'en ai déjà parlé dans ce cours mais on va un peu faire le point. Quand vous installez Laravel avec Composer vous trouvez à la racine le fichier .env avec ce contenu :

1. APP_ENV=local

2. APP_KEY=

3. APP_DEBUG=true

4. APP_LOG_LEVEL=debug

5. APP_URL=<a href="http://localhost">http://localhost</a>

7. DB_CONNECTION=mysql

8. DB_HOST=127.0.0.1

9. DB_PORT=3306

10. DB_DATABASE=homestead

11. DB_USERNAME=homestead

12. DB_PASSWORD=secret

14. BROADCAST_DRIVER=log

15. CACHE_DRIVER=file

16. SESSION_DRIVER=file

17. QUEUE_DRIVER=sync

19. REDIS_HOST=127.0.0.1

20. REDIS_PASSWORD=null

21. REDIS_PORT=6379

23. MAIL_DRIVER=smtp

24. MAIL_HOST=mailtrap.io

25. MAIL_PORT=2525

26. MAIL_USERNAME=null

27. MAIL_PASSWORD=null

28. MAIL_ENCRYPTION=null

30. PUSHER_APP_ID=

31. PUSHER_KEY=

32. PUSHER_SECRET=

Ça se présente sous la forme clé/valeur. Par exemple, on a déjà vu cela en œuvre dans config/app.php :

'debug' => env('APP_DEBUG', false),

L'helper env permet d'aller lire la valeur de APP_DEBUG dans le fichier .env et d'affecter la valeur de la clé debug de la configuration de l'application.

On trouve aussi par exemple le réglage de MySQL dans config/database.php :

1. 'host'      => env('DB_HOST', 'localhost'),

2. 'port'      => env('DB_PORT', '3306'),

3. 'database'  => env('DB_DATABASE', 'forge'),

4. 'username'  => env('DB_USERNAME', 'forge'),

5. 'password'  => env('DB_PASSWORD', ''),

Remarquez que l'helper accepte une valeur par défaut comme deuxième paramètre.

Que fait cet helper ?

Tout simplement il stocke les valeurs dans la super globale d'environnement $_ENV du serveur.

‌Moralité : pour avoir plusieurs environnements il faut créer plusieurs fichiers .env.

Le cas le plus classique sera d'avoir un environnement local de développement et un autre de déploiement. Il faut juste ne pas se mélanger les pinceaux, en particulier ne pas enlever le fichier .gitignore de la racine qui prévoit de ne pas envoyer le fichier .env qui peut contenir des données sensibles (si vous utilisez git).

La variable APP_ENV dans le fichier .env est justement destinée à connaître l'environnement actuel :

APP_ENV=local

Lorsque je crée une application je fais une copie du fichier d'environnement pour régler mes variables en situation production. Pour ne pas qu'il y ait de confusion je le nomme de façon explicite :

Dans ce fichier je fixe toutes les valeurs nécessaires :

1. APP_ENV=production

2. APP_DEBUG=false

3. APP_KEY=5avz0M3IGPu4GFxBBLPhQs00MNJREzoL

4. ...

Il me suffit ensuite d'envoyer ce fichier-là sur le serveur et de le renommer.

Selon où vous hébergez votre application vous pouvez avoir la possibilité de définir les variables d'environnement sans avoir à utiliser le fichier .env. C'est le cas si vous utilisez par exemple Forge.

IX-B - Le déploiement

IX-B-1 - Le serveur

Laravel n'a pas besoin de grand-chose mais quand même :

- PHP >= 5.6.4 ;

- extension PHP PDO ;

- extension PHP OpenSSL ;

- extension PHP Mbstring ;

- extension PHP Tokenizer.

D'autres part les dossiers storage et bootstrap/cache doivent avoir les droits d'écriture sur le serveur.

Selon la méthode d'installation que vous allez employer vérifiez que vous avez bien une clé de cryptage dans votre environnement.

IX-B-2 - L'envoi des fichiers de l'application

Il existe de nombreux outils pour envoyer ses fichiers sur le serveur de production mais c'est un sujet assez vaste et pas forcément facile. Dans le cadre de ce cours je me contenterai de parler de l'envoi avec FTP qui est la méthode la plus simple et classique.

Mais avant d'envoyer les fichiers il faut être sûr de ne pas avoir des choses inutiles.

La première chose à faire est de vider le cache qui peut être volumineux  :

php artisan cache:clear

La seconde chose est d'envoyer le bon fichier de configuration dont je vous ai parlé ci-dessus.

Et le dossier vendor ?

Là ça dépend si vous disposez de SSH sur le serveur, ce qui vous permet d'utiliser Composer (même s'il n'est pas installé globalement vous pouvez toujours envoyer le fichier phar) et ainsi d'éviter l'envoi du dossier vendor parce que vous pourrez exécuter un composer install.

Si vous ne disposez pas de SSH sur votre serveur vous pouvez soit changer d'hébergeur, soit envoyer le dossier vendor et tout son contenu.

IX-B-3 - L'installation

Une fois que les fichiers de l'application sont sur le serveur, si vous avez SSH (et donc vous n'avez pas envoyé le dossier vendor), vous devez procéder à l'installation.

Soit vous avez Composer installé globalement sur le serveur :

composer install

soit vous avez envoyé le fichier phar :

php composer.phar install

et là le dossier vendor va se créer et se remplir.

IX-B-4 - Le cache‌

Pour gagner en performance vous pouvez mettre les routes en cache pour en disposer plus rapidement :

Attention ! Si vous avez les routes en cache et que vous modifiez le fichier de routes il faudra régénérer le cache !

De la même manière vous pouvez mettre en cache les fichiers de configuration :

IX-B-5 - La base de données

Il vous faut également créer la base de données sur le serveur et bien renseigner les paramètres dans votre fichier .env de production. Ensuite si vous dispose de SSH lancez les migrations et la population (si nécessaire) sur le serveur :

php artisan migrate --seed

Si vous ne disposez pas de SSH l'affaire se complique un peu. Vous devez exporter votre base sur le serveur MySQL local et avec le fichier SQL généré créer votre base sur le serveur MySQL distant.

Avec tout ça votre application devrait fonctionner !

IX-C - Les mises à jour ultérieures

IX-C-1 - Mise à jour du framework

Pour effectuer les mises à jours ultérieures du framework il faudra utiliser :

composer update

Et évidemment si vous ne disposez pas de SSH vous devez faire la mise à jour en local et transférer ensuite les fichiers ajoutés et modifiés du dossier vendor.

IX-C-2 - Mise à jour de l'application

Quand vous mettez à jour votre application il peut y avoir des moments où il vaut mieux que personne ne se connecte. Laravel propose un mode maintenance facile à mettre en œuvre avec Artisan :

php artisan down

Et bien sûr il y a la commande inverse :

php artisan up

Évidemment encore faut-il que vous ayez accès à Artisan sur votre serveur…

En mode maintenance la vue affichée est celle située ici : resources/views/errors/503.blade.php

IX-D - Installation sur serveur mutualisé

Sur un serveur mutualisé peut se présenter une difficulté. On a vu que le seul dossier de Laravel qui doit être accessible est public, les autres ne doivent pas l'être. En général on fait pointer son domaine (ou sous-domaine) sur le dossier public et le tour est joué.

Mais parfois, on ne dispose que d'un dossier genre public_html et on doit tout mettre dedans. Laravel n'a pas été conçu pour ça mais on peut quand même le faire fonctionner ainsi. Voici la procédure :

commencez par installer normalement Laravel. Vous devez avoir cette architecture :

On va commencer par transférer tout ce qui se trouve dans le dossier public à la racine et supprimer ce dossier. Vous devez avoir maintenant cette situation :

Évidemment maintenant plus rien ne fonctionne !

Ouvrez le fichier index.php et trouvez cette ligne :

require __DIR__.'/../bootstrap/autoload.php';

Remplacez-la par celle-ci :

require __DIR__.'/bootstrap/autoload.php';

Dans le même fichier trouvez cette ligne :

$app = require_once __DIR__.'/../bootstrap/app.php';

Remplacez-la par celle-ci :

$app = require_once __DIR__.'/bootstrap/app.php';

Ces modifications sont nécessaires à cause du déplacement des fichiers et de la modification des emplacements relatifs.

Maintenant la page d'accueil de Laravel va s'afficher correctement.

Le souci c'est que maintenant tout devient accessible et il faut prendre des précautions !

Par exemple, en prévoyant dans les dossiers que vous voulez inaccessibles (app, bootstrap…) un .htaccess avec :

Deny from all

Les seuls fichiers accessibles restent ceux qui sont présents à la racine et il y a encore des choses sensibles comme votre fichier de configuration. Il faut donc aussi prévoir quelque chose pour .htaccess à ce niveau !

IX-E - Créer une installation comme WordPress

Si vous avez besoin d'une installation assistée genre celle proposée par WordPress j'ai créé un package qui peut vous aider.

Pour voir comment ça se présente commencez par créer une installation fraîche de Laravel.

Ajoutez l'authentification avec Artisan :

php artisan make:auth

Vérifiez que tout fonctionne correctement.

Créez aussi une base de données MySQL sur le serveur et notez son nom et les paramètres pour y accéder.

Ensuite installez le package :

composer require bestmomo/laravel5-3-installer

Attendez que l'installation soit terminée.

Ajoutez la référence du package dans config/app.php :

Bestmomo\Installer\InstallerServiceProvider::class,

Pour terminer publiez la configuration, les langues, les vues et les routes :

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

C'est pratiquement terminé !

Voyons comment ça fonctionne…

IX-E-1 - Étape 1 : vérification du serveur

Votre page d'accueil a changé pour celle-ci :

L'aspect ici est avec la locale en français, il y a aussi les textes en anglais.

Si vous arrivez sur cette page c'est que le serveur est correct pour Laravel (version et extensions de PHP, permissions sur les dossiers). Pour changer des éléments à ce niveau ça se passe dans le fichier de configuration :

Si vous voulez modifier les textes, en particulier le titre et le numéro de version, vous pouvez le faire dans le fichier des langues, par exemple pour le français :

IX-E-2 - Étape 2 : la base de données

Dans l'étape suivante on demande les informations pour la connexion à la base :

Modifiez les informations en conséquence. S'il y a une erreur vous recevez ce message :

Si tout se passe bien les migrations et les populations éventuelles sont lancées.

IX-E-3 - Étape 3 (optionnelle) : création d'un administrateur

Cette étape a lieu par défaut, vous pouvez changer ce comportement dans le fichier config/installer.php :

'administrator' => false,

Voici le formulaire qui apparaît :

Les éléments du formulaire sont aussi paramétrables dans le fichier de configuration.

Si tout se passe bien l'administrateur est créé et une nouvelle clé de cryptage est enregistrée dans le fichier .env.

Vous devez voir apparaître ce message :

À la fin de l'installation la référence au fournisseur de service de l'installeur est supprimée dans le fichier config/app.php, ce qui supprime les accès à cet installeur.

Vous avez ainsi tout ce qu'il faut pour créer une belle application et la distribuer :

- créez l'application complète avec le vendor ;

- ajoutez le package ;

- ajustez les paramètres dans la configuration du package.

IX-F - En résumé

- Il faut créer des environnements pour répondre aux différentes situations : local, distant…

- Le déploiement est facile et rapide si on dispose du SSH.

- On peut supprimer le dossier public tout en ayant un Laravel fonctionnel, il faut juste prendre des mesures pour assurer la sécurité.

- Le package bestmomo/laravel5-3-installer permet de créer une installation dans le style de WordPress.

X - Remerciements

Nous remercions Maurice Chavelli qui nous a autorisés de publier ce tutoriel.

Nous tenons également à remercier Winjerome pour la gabarisation et Jacques_jean pour la correction orthographique.