Tutoriel pour apprendre à utiliser le framework Laravel 4

Maurice Chavelli

Ce tutoriel a pour objectif de vous apprendre à utiliser le framework Laravel 4. Laravel est un framework PHP gratuit, open source, créé par Taylor Otwell. Il est destiné au développement d'applications Web suivant le modèle architectural modèle-vue-contrôleur.

Commentez

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

- Auteur : Maurice Chavelli

- Parution : 25 avril 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. Chapitre 1 : Présentation du framework

- I-A. C'est quoi un framework ?

- I-A-1. Pourquoi Laravel ?

- I-A-2. Le site de Laravel

- I-A-3. Télécharger Laravel 4

- I-A-4. Clonage avec Git

- II. Chapitre 2 : Installation

- II-A. Prérequis

- II-A-1. Composer

- II-A-2. Installation en un clic

- II-A-2-a. Méthode 1

- II-A-2-b. Méthode 2

- II-A-3. Installation plus laborieuse

- II-A-4. Résultat de l'installation

- III. Chapitre 3 : Architecture et configuration

- III-A. Le dossier public

- III-A-1. Le fichier index.php

- III-A-2. Le dossier app

- III-A-3. Configuration

- III-A-4. L'environnement

- IV. Chapitre 4 : Les routes

- IV-A. Structure des URL

- IV-A-1. Les routes de base

- IV-A-2. Une route avec post

- IV-A-3. Les paramètres

- IV-A-4. Les contraintes

- IV-A-5. Nommer les routes et redirections

- IV-A-6. Groupement de routes et préfixage

- V. Chapitre 5 : Les entrées

- V-A. Récupérer des données transmises

- V-A-1. Tester les données transmises et les valeurs par défaut

- V-A-2. Les données dans un tableau

- V-A-3. Les fichiers

- VI. Chapitre 6 : Les vues

- VI-A. Une vue simple

- VI-A-1. Des paramètres

- VI-A-2. Une sous-vue

- VI-A-3. Le composeur de vue

- VII. Chapitre 7 : Un exemple de site statique

- VII-A. Création de la page

- VII-A-1. Le template

- VII-A-2. Les pages

- VII-A-3. La route

- VIII. Chapitre 8 : Blade

- VIII-A. Syntaxe de base

- VIII-A-1. Les conditions

- VIII-A-2. Mise en page

- VIII-A-3. Un exemple

- IX. Chapitre 9 : Les contrôleurs

- IX-A. Créer un contrôleur

- IX-A-1. Un contrôleur « RESTful »

- IX-A-2. Créer un contrôleur de ressource avec artisan

- X. Chapitre 10 : Les filtres

- X-A. Notre premier filtre

- X-A-1. Appliquer plusieurs filtres

- X-A-2. Appliquer un filtre à plusieurs routes

- X-A-3. Filtres sur les contrôleurs

- XI. Chapitre 11 : Gestion des erreurs

- XI-A. Le fichier de « log »

- XI-A-1. Les erreurs HTTP

- XII. Chapitre 12 : Les formulaires

- XII-A. Ouvrir et fermer un formulaire

- XII-A-1. Protection CSRF

- XII-A-2. Les champs texte

- XII-A-3. Les nouveaux types du HTML 5

- XII-A-4. Les légendes

- XII-A-5. Les boutons radio

- XII-A-6. Les cases à cocher

- XII-A-7. Les listes de choix

- XII-A-8. Les boutons

- XII-A-9. Pointer sur une route nommée

- XII-A-10. Créer des macros

- XII-A-11. Renseigner les champs d'un formulaire

- XIII. Chapitre 13 : Les bases de données 1/3

- XIII-A. Configuration

- XIII-A-1. Migration

- XIII-A-2. Le Schema builder

- XIII-A-3. Des données d'exemple

- XIII-A-4. Les modifications ultérieures

- XIII-A-5. Scénario de migrations

- XIII-A-5-a. Installation

- XIII-A-5-b. Première migration

- XIII-A-5-c. Ajout d'un champ

- XIII-A-5-d. Retour en arrière

- XIII-A-5-e. Ajout d'une table

- XIII-A-5-f. Épurer une migration

- XIII-A-5-g. Rafraichissement des migrations

- XIII-A-5-h. Suppression de toutes les migrations

- XIV. Chapitre 14 : Les bases de données 2/3

- XIV-A. Des tables d'exemple

- XIV-A-1. Insertion de données

- XIV-A-2. Sélectionner des données

- XIV-A-3. Les jointures

- XIV-A-4. Mise à jour et suppression d'enregistrements

- XV. Chapitre 15 : Les bases de données 3/3

- XV-A. Créer un modèle avec Eloquent

- XV-A-1. Extraire des données

- XV-A-2. Insérer des données

- XV-A-3. Modifier et supprimer des données

- XV-A-4. Les relations

- XV-A-5. Inverse d'une relation

- XV-A-6. Chargement dynamique

- XVI. Chapitre 16 : Authentification

- XVI-A. La table des utilisateurs

- XVI-A-1. Insertion d'utilisateurs

- XVI-A-2. Un formulaire de connexion

- XVI-A-3. Traitement de la réponse au formulaire

- XVI-A-4. Filtrage des utilisateurs

- XVI-A-5. Se rappeler de moi

- XVI-A-6. Protection CSRF

- XVII. Chapitre 17 : Validation

- XVII-A. Le formulaire d'inscription

- XVII-A-1. Les routes

- XVII-A-2. La validation

- XVII-A-3. Les messages

- XVII-A-4. Utilisation des messages

- XVII-A-5. Renseigner les champs au retour d'erreurs de saisie

- XVIII. Chapitre 18 : Un blog : les données

- XVIII-A. Installer et configurer Laravel

- XVIII-A-1. La base de données

- XVIII-A-2. Créer une migration

- XVIII-A-3. Le schéma des tables

- XVIII-A-4. Des données

- XVIII-A-5. Les modèles

- XVIII-B. Migration pour l'authentification

- XIX. Chapitre 19 : Un blog : le template et les routes

- XIX-A. L'aspect du blog

- XIX-A-1. Le template

- XIX-A-2. Les routes

- XIX-A-3. La pagination

- XIX-A-4. Injecter l'instance d'un modèle

- XIX-A-5. Le formulaire de recherche

- XIX-A-6. Le groupement des filtres

- XX. Chapitre 20 : Quel IDE pour Laravel 4 ?

- XXI. Chapitre 21 : Un blog : utilisation de contrôleurs

- XXI-A. Les routes

- XXI-B. Le contrôleur

- XXI-C. Les vues

- XXI-D. Conclusion

- XXII. Chapitre 22 : Installer des générateurs

- XXII-A. Installation du paquetage

- XXII-A-1. Créer une migration

- XXII-A-1-a. Créer une table

- XXII-A-1-b. Ajouter un champ

- XXII-A-1-c. Supprimer un champ

- XXII-A-2. Créer un modèle

- XXII-A-3. Créer des seeds

- XXII-A-4. Créer une vue

- XXII-A-5. Créer une ressource

- XXII-A-6. Utilisation de la ressource

- XXII-A-6-a. Index

- XXII-A-6-b. Show

- XXII-A-6-c. Create

- XXII-A-6-d. Edit

- XXII-A-6-e. Destroy

- XXII-A-6-f. Synthèse

- XXII-A-7. Créer un « scaffold »

- XXII-A-8. Générer un formulaire

- XXIII. Chapitre 23 : Les helpers

- XXIII-A. La classe « HTML »

- XXIII-A-1. La méthode « entities »

- XXIII-A-1-a. Méthode « decode »

- XXIII-A-1-b. Méthode « image »

- XXIII-A-1-c. Méthode « link »

- XXIII-A-1-d. Méthode « secureLink »

- XXIII-A-1-e. Méthode « script »

- XXIII-A-1-f. Méthode « style »

- XXIII-A-1-g. Méthode « linkRoute »

- XXIII-A-1-h. Méthode « linkAction »

- XXIII-A-1-i. Méthode « ol »

- XXIII-A-1-j. Méthode « ul »

- XXIII-A-1-k. Les macros

- XXIII-A-2. Les URL

- XXIII-A-2-a. Méthode « url »

- XXIII-A-2-b. Méthode « route »

- XXIII-A-2-c. Méthode « action »

- XXIII-A-2-d.  Méthode « asset »

- XXIII-A-3. Les liens

- XXIII-A-3-a. Méthode « link_to »

- XXIII-A-3-b. Méthode « link_to_route »

- XXIII-A-3-c. Méthode « link_to_action »

- XXIII-A-4. Les chaînes de caractères

- XXIII-A-4-a. Méthode « starts_with »

- XXIII-A-4-b. Méthode « ends_with »

- XXIII-A-4-c. Méthode « str_contains »

- XXIII-A-4-d. Méthode « str_is »

- XXIII-A-4-e. Méthode « str_random »

- XXIII-A-4-f. Méthode « snake_case »

- XXIII-A-4-g. Méthode « camel_case »

- XXIII-A-4-h. Méthode « e »

- XXIII-A-4-i. Méthode slug

- XXIII-A-4-j. Méthode title

- XXIII-A-4-k. Méthode finish

- XXIII-A-4-l. Méthode limit

- XXIII-A-4-m. Méthode words

- XXIII-A-5. Les tableaux

- XXIII-A-5-a. Méthode « array_divide »

- XXIII-A-5-b. Méthode « head »

- XXIII-A-5-c. Méthode « last »

- XXIII-A-5-d. Méthode « array_dot »

- XXIII-A-5-e. Méthode « array_fetch »

- XXIII-A-5-f. Méthode « array_flatten »

- XXIII-A-5-g. Méthode « array_first »

- XXIII-A-5-h. Méthode « array_only »

- XXIII-A-5-i. Méthode « array_except »

- XXIII-A-5-j. Méthode « array_get »

- XXIII-A-5-k. Méthode « array_set »

- XXIII-A-5-l. Méthode « array_forget »

- XXIII-A-5-m.  Méthode « array_pull »

- XXIII-A-5-n. Méthode « array_pluck »

- XXIV. Chapitre 24 : Un blog : authentification

- XXIV-A. Contrôleur HomeController

- XXIV-B. Vues

- XXIV-B-1. La navigation

- XXIV-B-2. Le formulaire de connexion

- XXIV-B-3. La langue

- XXIV-C. Les routes

- XXIV-D. Le contrôleur AuthController

- XXIV-E. La connexion

- XXIV-E-1. Échec de la connexion

- XXIV-E-2. Réussite de la connexion

- XXIV-F. La déconnexion

- XXIV-G. Inscription d'un utilisateur

- XXIV-G-1. La vue

- XXIV-G-2. La validation

- XXIV-G-3. Le traitement

- XXIV-H. La réinitialisation du mot de passe

- XXIV-H-1. Le contrôleur et la route

- XXIV-H-2. Le formulaire de demande de réinitialisation

- XXIV-H-3. Le traitement de la demande

- XXV. Chapitre 25 : Un blog : les tests unitaires

- XXV-A. C'est quoi un test unitaire ?

- XXV-A-1. Que faut-il pour faire des tests ?

- XXV-A-2. Principes de base de PHPUnit

- XXV-A-3. Configurer la base de données pour les tests

- XXV-A-4. Les fichier TestCase

- XXV-A-5. Architecture des dossiers

- XXV-A-6. Notre premier test

- XXV-A-7. Les tests pour HomeController

- XXV-A-7-a. Actions « accueil », « categorie » et « article »

- XXV-A-7-b. Action « find »

- XXV-A-7-c. Action « comment »

- XXV-A-8. Les tests pour AuthController

- XXV-A-9. Les tests pour GuestController

- XXVI. Chapitre 26 : Création d'un paquetage

- XXVI-A. Le workbench

- XXVI-A-1. Le fichier FormulorServiceProvider.php

- XXVI-A-2. Le code de formulor

- XXVI-A-3. La façade

- XXVI-A-4. Le fichier composer.json

- XXVI-A-5. Utilisation du paquetage directement

- XXVI-A-6. Un packetage avec CKEditor

- XXVII. Chapitre 27 : Cache et configuration

- XXVII-A. Les commandes de base

- XXVII-A-1. Un cas d'application

- XXVII-A-1-a. La route

- XXVII-A-1-b. Le contrôleur

- XXVII-A-1-c. Les vues

- XXVII-A-1-d. Les assets

- XXVII-A-2. Mise en cache pour l'application

- XXVII-A-3. Configuration

- XXVIII. Chapitre 28 : Créer une commande

- XXVIII-A. Générer la classe

- XXVIII-A-1. Argument et options

- XXVIII-A-2. Codage de la commande

- XXIX. Chapitre 29 : Le routage

- XXIX-A. Préambule

- XXIX-B. Les verbes

- XXIX-C. Écriture des routes

- XXIX-D. Les paramètres

- XXIX-D-1. Présentation

- XXIX-D-2. Paramètre optionnel

- XXIX-D-3. Contraintes sur les paramètres

- XXIX-E. Grouper des routes

- XXIX-F. Les contrôleurs

- XXIX-F-1. Les contrôleurs RESTful

- XXIX-G. Les contrôleurs de ressources

- XXIX-H. Ressources imbriquées

- XXIX-I. Contrainte pour les contrôleurs

- XXIX-J. Redirections et routes nommées

- XXIX-K. Les méthodes back et refresh

- XXIX-L. Injection d'un modèle

- XXX. Chapitre 30 : Le filtrage

- XXX-A. Les filtres par défaut

- XXX-A-1. Les filtres globaux

- XXX-B. Les filtres d'authentification

- XXX-C. Le filtre CSRF

- XXX-D. Créer un filtre

- XXX-E. Plusieurs filtres

- XXX-F. Paramètres dans les filtres

- XXX-F-1. Un paramètre

- XXX-F-2. Plusieurs paramètres

- XXX-G. Un patron de route

- XXX-H. Les classes de filtres

- XXX-I. Filtres sur les contrôleurs

- XXX-I-1. Filtre déclaré dans la route

- XXX-I-2. Filtre déclaré dans le contrôleur

- XXX-I-3. Filtre dans le contrôleur

- XXX-I-4. Filtres paramétrés

- XXXI. Chapitre 31 : Le conteneur de dépendances (IoC)

- XXXI-A. L'objet Application

- XXXI-B. Gérer du contenu

- XXXI-C. Liaison avec le conteneur

- XXXI-D. Liaison de classes et injection de dépendance

- XXXI-E. Singleton et objet existant

- XXXI-F. Les Services Providers

- XXXI-G. En analysant le code de Laravel

- XXXII. Chapitre 32 : Organiser son code

- XXXII-A. Les trois façons d'ajouter du code

- XXXII-A-1. Ajout de fonctions

- XXXII-A-2. Ajout de classes

- XXXII-A-3. PSR-0

- XXXII-B. Un cas d'exemple

- XXXII-C. La validation dans des fonctions

- XXXII-D. La validation dans des classes

- XXXII-E. Les espaces de noms

- XXXII-E-1. psr-0 kesaco ?

- XXXII-E-2. Utilisation de psr-0 pour l'exemple

- XXXII-F. Délocaliser la gestion du modèle

- XXXII-G. Un service de validation

- XXXIII. Chapitre 33 : Les relations avec Eloquent 1/2

- XXXIII-A. La base d'exemple

- XXXIII-B. La relation 1:1

- XXXIII-B-1. hasOne

- XXXIII-B-2. belongsTo

- XXXIII-C. La relation 1:n

- XXXIII-C-1. hasMany

- XXXIII-C-2. belongsTo

- XXXIII-C-3. hasManyThrough

- XXXIII-D. La relation n:n

- XXXIII-D-1. belongsToMany

- XXXIII-E. Relation polymorphique de type 1:n

- XXXIII-E-1. morphTo

- XXXIII-E-2. morphMany

- XXXIII-F. Relation polymorphique de type n:n

- XXXIII-F-1. morphToMany

- XXXIII-F-2. morphedByMany

- XXXIV. Chapitre 34 : Les relations avec Eloquent 2/2

- XXXIV-A. La base

- XXXIV-B. Le code

- XXXIV-B-1. Installation

- XXXIV-B-2. Organisation du code

- XXXIV-B-3. Les macros

- XXXIV-B-4. Les routes

- XXXIV-B-5. Les contrôleurs

- XXXIV-B-6. Considérations générales

- XXXIV-C. Gestion des pays

- XXXIV-C-1. Le modèle

- XXXIV-C-2. Le contrôleur

- XXXIV-C-3. La liste

- XXXIV-C-4. La fiche du pays

- XXXIV-C-5. Modifier un pays

- XXXIV-C-6. Détruire un pays

- XXXIV-D. Gestion des villes

- XXXIV-D-1. La fiche de la ville

- XXXIV-D-2. Modifier une ville

- XXXIV-D-3. Créer une ville

- XXXIV-E. Gestion des livres

- XXXIV-E-1. La fiche du livre

- XXXIV-E-2. Modifier un livre

- XXXIV-E-3. Transaction ?

- XXXIV-E-4. Création d'un livre

- XXXIV-E-5. Destruction d'un livre

- XXXIV-F. Gestion des éditeurs

- XXXIV-G. Gestion des thèmes

- XXXIV-G-1. La fiche du thème

- XXXIV-G-2. La modification d'un thème

- XXXIV-G-3. Créer un thème

- XXXIV-G-4. Supprimer un thème

- XXXV. Chapitre 35 : Les composeurs de vues

- XXXV-A. Les composeurs de vues

- XXXV-B. Format du menu

- XXXV-C. Organisation du code

- XXXV-D. Le composeur

- XXXV-E. Les vues

- XXXV-E-1. Le template

- XXXV-E-2. La navigation

- XXXV-E-3. Les autres vues

- XXXV-F. Les routes

- XXXV-G. Le résultat

- XXXVI. Chapitre 36 : Le CMS October

- XXXVI-A. Installation d'October

- XXXVI-B. Le frontend

- XXXVI-B-1. Le layout

- XXXVI-B-2. Les pages

- XXXVI-B-3. Les partials

- XXXVI-B-4. Les contents blocks

- XXXVI-B-5. AJAX

- XXXVI-C. Le back-end

- XXXVI-D. Les plugins

- XXXVI-E. Conclusion

- XXXVII. Remerciements

I - Chapitre 1 : Présentation du framework

I-A - C'est quoi un framework ?

C'est la bonne question à se poser au départ. Qu'est-ce qu'un framework ?

Voilà la définition qu'en offre Wikipédia :

« Un framework est un ensemble d'outils et de composants logiciels organisés conformément à un plan d'architecture et à des patterns, l'ensemble formant ou promouvant un squelette de programme. »

Détaillons un peu cette définition :

- un ensemble d'outils : on aborde directement l'aspect instrumental. Un framework n'est pas un pur concept, mais un ensemble concret d'outils ;

- un ensemble de composants logiciels : on dispose donc de « briques » logicielles toutes prêtes pour bâtir nos applications ;

- organisés conformément à un plan d'architecture et des patterns : les outils et les briques logicielles ne sont pas empilés en désordre, mais organisés selon une certaine logique, chaque élément étant bien à sa place. Cette architecture implique un modèle de développement, une sorte de guide, donc on ne dispose pas seulement d'éléments prêts à l'emploi, mais en plus on nous accompagne dans la façon de les utiliser ;

- l'ensemble formant ou promouvant un squelette de programme : en fait un framework nous offre l'essentiel de notre application, l'image du squelette est éloquente, on a une structure solide, bien établie : à nous d'ajouter de la vie.

Un framework s'adresse à un développeur, il est là pour lui faciliter la vie, lui faire gagner du temps, le guider dans l'organisation de son code, l'aider à le tester et le mettre à jour.

I-A-1 - Pourquoi Laravel ?

On rencontre souvent dans les forums la question de savoir quel est le meilleur framework. La réponse la plus intelligente à donner est que le meilleur est celui qu'on connaît bien et qui répond à la fois à nos besoins et à notre compétence. Il y a toutefois des critères objectifs dans le choix d'un framework :

- la documentation : un framework mal documenté fait perdre plus de temps qu'il n'en fait gagner. Laravel est très bien documenté ;

- la communauté : lorsqu'on rencontre une difficulté, il est bon de recevoir une aide rapide pour ne pas rester bloqué longtemps sur un problème de détail. La communauté de Laravel est réactive ;

- la durée d'apprentissage : un temps d'apprentissage trop long fait perdre l'intérêt d'un framework même si on factorise notre connaissance en multipliant les cas d'application. Laravel est rapide à prendre en main tout en ne faisant aucune concession sur les fonctionnalités ;

- la pérennité : apprendre un framework pour apprendre quelques mois plus tard qu'il est abandonné est plutôt frustrant. Laravel est soutenu par une importante communauté de développeurs et d'utilisateurs, ce qui lui garantit une durée de vie raisonnable ;

- la richesse des outils : certains frameworks sont intéressants mais nécessitent de nombreuses greffes pour devenir vraiment opérationnels. Laravel est à la base suffisamment complet pour ne nécessiter que des extensions pour des besoins particuliers ;

- la simplicité : utiliser un framework et se retrouver à écrire un code plus complexe que celui qu'on aurait utilisé avec simplement du PHP n'est pas une opération judicieuse. Le code qu'on écrit avec Laravel est toujours simple, lisible, concis ;

- la version de PHP utilisée : Laravel est fondé sur PHP 5.3, ce qui permet d'utiliser par exemple les fonctions anonymes (lambda), les fermetures, les espaces de noms…

- les mises à jour : on n'est jamais à l'abri d'une faille de sécurité, le fait d'utiliser un framework est un peu à double facette : le code a été normalement bien écrit, validé, mais il est lisible par tout le monde et devient par le fait vulnérable, d'où la nécessité de procéder à des mises à jour en cas de découverte d'une faille. L'équipe de Laravel est assez réactive pour assumer cette tâche essentielle ;

- les standards de programmation : le code d'un « bon » framework est écrit en suivant les « bonnes pratiques » et il nous invite à faire de même. Laravel a souvent été salué pour la pureté de son code ;

- Les possibilités d'extension : on doit pouvoir facilement étendre les fonctionnalités d'un framework sans détruire son organisation. Laravel permet de le faire avec élégance en suivant toujours les mêmes principes avec ses bundles (paquets).

Convaincu ? Alors poursuivons notre découverte de Laravel…

I-A-2 - Le site de Laravel

Le site de Laravel est aussi simple et épuré que le framework qu'il promeut :

Il contient :

- un historique de ce framework créé par Taylor Otwell en avril 2011 ;

- une collection de bundles à télécharger (ce sont des « briques » logicielles pour étendre le framework) ;

- une documentation détaillée ;

- des liens vers des ressources externes ;

- un lien de téléchargement.

Il y a également ce blog en français qui est déjà plus pointu et pourra vous apporter certaines réponses lorsque vous maîtriserez les bases du framework.

I-A-3 - Télécharger Laravel 4

Pour télécharger Laravel 4 il suffit d'un petit clic ici et de télécharger le fichier ZIP.

Vous obtenez un fichier compressé contenant ces éléments :

Quelques fichiers et trois dossiers. Nous verrons tout cela en détail au cours de ce tutoriel. La première chose à prendre en compte est que le framework n'est pas dans ce paquetage, on y retrouve juste l'architecture. Depuis la version 4 le framework utilise « Composer » pour télécharger les fichiers et les mettre à jour, c'est pour cette raison que vous avez un paquetage réduit à un simple squelette émaillé de quelques fichiers.

I-A-4 - Clonage avec Git

Au lieu de télécharger les fichiers comme nous venons de le voir vous pouvez aussi utiliser Git pour les cloner dans un dossier. Par exemple :

Il faut évidemment avoir Git installé sur votre machine. Mais de toute façon il vous le faudra pour faire fonctionner composer, comme nous le verrons dans le prochain chapitre.

Vous trouverez la documentation originale de Laravel 4 ici.

Maintenant que nous avons vu toutes ces généralités, nous allons passer à des choses plus concrètes…

II - Chapitre 2 : Installation

Nous avons vu précédemment où trouver le paquetage d'installation. Voyons à présent comment l'installer et le configurer.

II-A - Prérequis

Laravel nécessite un serveur Apache avec au minimum PHP 5.3 et l'extension Mcrypt.

Vous pouvez installer en local un pack comme Wamp (c'est celui que j'utilise personnellement), Xampp ou EasyPHP.

Vérifiez que vous avez l'extension Mcrypt activée.

II-A-1 - Composer

Laravel 4 utilise Composer, un gestionnaire de dépendances, pour son installation.

Les frameworks sont composés de nombreux éléments et utilisent souvent des applications « tierces », c'est-à-dire qui sont développées dans le cadre d'autres projets. Ces applications peuvent avoir elles-mêmes d'autres applications « tierces ». Avoir en permanence tous ces éléments à jour est un défi qui n'est pas facile à assumer dans le cadre d'un paquetage figé. C'est pour cette raison que les gestionnaires de dépendances ont été créés.

L'objectif d'un gestionnaire de dépendances est de télécharger les dernières versions de ces applications et de les conserver à jour. Composer est le gestionnaire le plus récent et sans aucun doute le plus abouti, en tout cas c'est celui qui a été choisi pour l'installation de Laravel. Il offre aussi l'avantage de créer un fichier pour générer automatiquement des références de chargement automatique de classes (Laravel 4 respecte les PSR-0 et PSR-1 du fig-standards). Si le chargement de classe ne vous est pas familier, allez faire un tour sur ce chapitre.

Il vous faut donc installer Composer, ce qui est relativement facile. Vous avez toutes les informations requises ici : http://getcomposer.org/doc/00-intro.md.

Composer ne fonctionnera pas si vous n'avez pas Git installé sur votre ordinateur. Vous pouvez télécharger la version correspondant à votre système ici.

Vous n'avez pas besoin de maîtriser ces deux applications pour installer Laravel, nous allons voir les commandes de base pour télécharger les fichiers.

Vérifiez que Composer fonctionne en utilisant simplement la commande composer à partir de l'invite de commande. Vous devez voir apparaître un logo d'accueil ainsi qu'une liste des commandes disponibles :

II-A-2 - Installation en un clic

II-A-2-a - Méthode 1

La façon la plus simple d'installer Laravel est d'utiliser la commande create-project de Composer :

Il suffit d'attendre quelques minutes pour avoir une installation complète !

II-A-2-b - Méthode 2

Vous pouvez aussi utiliser l'archive phar de l'installateur de Laravel. Vous la placez sur votre serveur et vous entrez laravel new pour créer directement une nouvelle version de Laravel dans le dossier. Vous pouvez aussi pointer un autre dossier avec laravel new mon_dossier.

II-A-3 - Installation plus laborieuse

Vous pouvez télécharger le pack de base comme nous l'avons vu au chapitre précédent dans un dossier de votre serveur. Dans mon cas j'ai créé un dossier laravel :

Ouvrez le fichier « composer.json » :

1. {

2. "name": "laravel/laravel",

3. "description": "The Laravel Framework.",

4. "keywords": ["framework", "laravel"],

5. "license": "MIT",

6. "require": {

7. "laravel/framework": "4.1.*"

8. },

9. "autoload": {

10. "classmap": [

11. "app/commands",

12. "app/controllers",

13. "app/models",

14. "app/database/migrations",

15. "app/database/seeds",

16. "app/tests/TestCase.php"

17. ]

18. },

19. "scripts": {

20. "post-install-cmd": [

21. "php artisan optimize"

22. ],

23. "post-update-cmd": [

24. "php artisan clear-compiled",

25. "php artisan optimize"

26. ],

27. "post-create-project-cmd": [

28. "php artisan key:generate"

29. ]

30. },

31. "config": {

32. "preferred-install": "dist"

33. },

34. "minimum-stability": "dev"

35. }

C'est le fichier qui va indiquer à Composer les applications à télécharger. La clé « require » indique qu'il faut aller chercher le paquetage laravel/framework dans sa version 4.1 la plus récente. Pour sa localisation c'est tout simple, c'est dans Github.com/laravel/framework. Pour la clé autoload, sont inclus les dossiers de Laravel qui nécessitent un chargement automatique des classes.

Pour finir l'installation de Laravel il suffit de taper composer install dans le dossier racine de l'application et d'attendre la fin des téléchargements :

II-A-4 - Résultat de l'installation

Si on regarde maintenant ce qu'il s'est passé, on découvre un nouveau dossier bien garni :

On trouve en particulier un fichier autoload.php qui est là pour gérer le chargement automatique dont j'ai parlé plus haut.

L'installation est maintenant terminée, vous n'avez plus qu'à vérifier que tout fonctionne en entrant l'URL de votre site et en pointant le répertoire public. Ce qui donne dans mon cas :http://localhost/laravel/public/

Normalement vous obtenez ça :

Si ce n'est pas le cas reprenez tout depuis le début .

Comment mettre à jour Laravel ? Mais c'est d'une simplicité enfantine, il suffit de taper la commande composer update !

Pour l'installation sur un serveur distant il faut disposer d'un accès SSH, il suffit alors d'utiliser composer.phar sur le serveur, si on dispose de toutes les commandes utiles, ce qui n'est pas souvent le cas sur les serveurs mutualisés. Sinon il faut tout transmettre en FTP.

III - Chapitre 3 : Architecture et configuration

Nous allons maintenant voir comment est organisé Laravel 4. À l'issue de l'installation nous avons quelques fichiers et quatre dossiers :

Le dossier public est le seul qui doit être accessible à l'utilisateur. C'est le dossier qui va héberger vos images, vos fichiers CSS, vos scripts.

Le dossier vendor contient toutes les bibliothèques utilisées par Laravel et vous n'avez normalement pas à intervenir directement dans celles-ci.

Le dossier app est celui qui va contenir votre application, c'est votre terrain de jeu, vous y mettrez vos modèles, vos contrôleurs, vos vues, vos assets.

Le dossier bootstrap contient des fichiers qui gèrent le démarrage de Laravel.

Voici un schéma résumé de cette organisation :

III-A - Le dossier public

Au départ ce dossier ne contient pas grand-chose, essentiellement le fichier index.php qui est la porte d'entrée de l'application et un fichier .htaccess :

Il contient aussi un fichier d'icône vide favicon.ico. Les navigateurs cherchent par défaut ce fichier pour afficher l'icône du site. Vous pouvez donc l'adapter à vos besoins (une image de 16 * 16 px).

Il contient aussi le fichier robots.txt qui indique aux robots d'indexation de sites les pages qu'ils doivent explorer. Par défaut tout est indexé. Si vous désirez un comportement différent, vous devez renseigner ce fichier en conséquence.

III-A-1 - Le fichier index.php

C'est la porte d'entrée unique de l'application. Il ne contient que très peu de code dont voici l'essentiel :

- Lancement du chargement automatique créé par Composer :

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

- Inclusion du fichier de démarrage :

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

- Lancement de l'application :

$app->run();

- Et pour finir :

$app->shutdown();

Vous voyez donc que l'essentiel de la machinerie se situe dans la partie cachée.

III-A-2 - Le dossier app

C'est le dossier dans lequel vous allez créer votre application, il contient deux fichiers et il est subdivisé en sous-dossiers.

Voyons d'abord les deux fichiers :

- routes : contient les définitions des chemins d'entrées pour l'utilisateur, autrement dit les URI possibles et les dirige sur la classe qui doit traiter l'information ;

- filters : contient les définitions des filtres appliqués aux chemins, autrement dit les actions à effectuer juste avant ou après.

Voyons maintenant les dossiers :

- commands : prévu pour contenir vos commandes personnelles pour « Artisan », un utilitaire en ligne de commande de Laravel ;

- config : contient des éléments de configuration pour changer l'aspect et le fonctionnement de Laravel ;

- controllers : contient les contrôleurs (schéma MVC). Il est à noter que l'utilisation de contrôleurs n'est pas indispensable avec ce framework parce que le système de routage est très performant comme nous le verrons par la suite ;

- lang : contient des fichiers avec du texte évidemment en anglais à la base ;

- models : contient les classes qui représentent le modèle de données de l'application (schéma MVC) ;

- start : contient les fichiers de démarrage de votre application ;

- storage : stockage d'informations temporaires diverses : sessions, logs, vues compilées, caches…

- views : contient les vues, ce sont les fichiers destinés à présenter la réponse à l'utilisateur (schéma MVC), utilisées par les routes et les contrôleurs ;

- tests : pour vos tests unitaires à lancer avec Artisan ;

- database : contient les migrations (modifications de la base de données) et les seeds (remplissage des données).

III-A-3 - Configuration

Laravel nécessite très peu de configuration. Ouvrez le fichier app/config/app.php et trouvez cette ligne :

'key' => 'YourSecretKey!!!',

Ici vous devez entrer votre clé secrète composée de 32 caractères alphanumériques. Il est possible d'en créer une dans l'invite de commande avec l'outil « Artisan ». Pour le moment vous pouvez l'utiliser pour générer la clé :

La clé est directement inscrite dans le fichier app/config/app.php.

Vous trouvez dans le même fichier cette ligne :

'locale' => 'en',

Évidemment vous pouvez entrer fr à la place, mais auparavant il aura fallu prévoir les fichiers correspondant avec la traduction ! Nous verrons cela plus tard, pour le moment laissez en.

Laravel est prévu à la base avec un fichier .htaccess pour ne pas voir apparaître index.php dans les URL :

1. <IfModule mod_rewrite.c>

2. Options -MultiViews

3. RewriteEngine On

4. RewriteCond %{REQUEST_FILENAME} !-d

5. RewriteCond %{REQUEST_FILENAME} !-f

6. RewriteRule ^ index.php [L]

7. </IfModule>

Mais évidemment ça ne fonctionne que si le module mod_rewrite est activé dans votre version d'Apache. Si ce n'est pas le cas, il faudra supporter des URI plus longs et moins esthétiques…

III-A-4 - L'environnement

Selon l'environnement dans lequel nous utilisons Laravel, nous n'avons pas les mêmes besoins. Par exemple nous allons éviter d'avoir des erreurs qui s'affichent sur un serveur de production alors que l'affichage des erreurs est utile lors du développement. Si nous utilisons une base de données elle ne sera pas forcément en localhost en production. En cours de développement nous voudrons aussi sans doute intercepter l'envoi des emails.

Regardez ce code dans le fichier bootstrap/start.php :

$env = $app->detectEnvironment(array(
'local' => array('homestead'),
));

Laravel détecte automatiquement l'environnement dans lequel il s'exécute. C'est ici qu'il va regarder pour définir l'environnement. On se rend compte qu'il a pour le moment peu de choix.

homestead fait référence à un environnement Vagrant disponible pour Laravel depuis la version 4.2.

C'est à vous de définir les environnements dont vous avez besoin. En général il vous suffit d'un environnement local de développement. Par exemple :

$environments = array(
'local' => array('nom_de_ma_machine'),
);

Ici j'ai défini l'environnement local en spécifiant le nom de ma machine (ce que retourne la fonction gethostname()). Il existe un dossier app/config/local. Il suffit de mettre dans ce dossier les fichiers de configuration avec seulement les valeurs que l'on veut spécifier. Par défaut il y a déjà un fichier app/php qui fixe la valeur du debug (débogage) à true. Il y a aussi un fichier database.php parce que forcément la configuration de la base sera différente en local et à distance.

Depuis la version 4.1 on ne prend plus en compte l'URL comme référence à cause de problèmes de sécurité.

IV - Chapitre 4 : Les routes

Voyons le fonctionnement d'une requête avec Laravel. Une requête commence par être traitée par le .htaccess pour être obligatoirement dirigée sur le fichier index.php. Celui-ci commence par charger le fichier autoload.php :

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

C'est à ce niveau que sont effectués les chargements des classes. Ensuite index.php charge le fichier start.php :

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

Ce fichier start.php se situe dans le dossier bootstrap de Laravel. C'est dans ce fichier que l'application est créée :

$app = new Illuminate\Foundation\Application;

L'objet application est le conteneur IoC. Ensuite sont définis le chemin de l'application, l'environnement, et le fichier des routes est chargé. On peut alors traiter la requête et envoyer une réponse au demandeur.

IV-A - Structure des URL

Les URL utilisés par Laravel ont cette structure :

http://mondomaine.org/ma/page

La partie ma/page peut être constituée par tout ce que vous voulez, avec autant de découpages que nécessaire, comme nous allons le voir tout au long de ce tutoriel.

IV-A-1 - Les routes de base

La requête est analysée et comparée aux chemins définis dans le fichier app/route.php.

Si vous l'ouvrez vous ne trouvez que ce code :

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

2. {

3. return View::make('hello');

4. });

Changez-le pour celui-ci :

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

2. {

3. return 'coucou';

4. });

Si vous lancez l'application avec l'URI http://localhost/laravel/public/ vous obtenez le message « coucou » à l'écran, ce qui montre que l'URI est bien traité par ce code (je rappelle que je pars de l'hypothèse que vous testez Laravel 4 en local dans un dossier nommé « laravel »).

La syntaxe est simple, vous indiquez le type de requête, ici on a get mais vous pouvez utiliser aussi post comme nous allons le voir plus loin (sont aussi gérés put et delete). Vous avez ensuite deux paramètres dont le premier est le chemin (ici c'est la racine représentée par « / »), le second paramètre est une fonction anonyme contenant le traitement.

Faisons un autre essai avec un autre URL :

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

2. {

3. return 'login';

4. });

Ici l'URI correspondant est http://localhost/laravel/public/login.

Remarquez qu'on n'ajoute pas le signe « / » dans ce cas devant « login ». Le seul cas où on l'utilise est celui de la racine. Par contre maintenant si vous utilisez l'URL http://localhost/laravel/public/ vous obtenez un message d'erreur puisque nous n'avons plus de chemin défini pour cet URL :

Sur un site en production il faudra évidemment éviter ce genre de message, mais en cours de développement c'est indispensable.

IV-A-2 - Une route avec post

Prenons un exemple de route avec une requête de type post. Pour cela créons un petit formulaire nommé test.html et placé dans le dossier public :

1. <form action="log" method="post">

2. <input type="text" name="nom" />

3. <input type="submit" name="Envoyer" />

4. </form>

Et prévoyons la route pour récupérer l'information :

1. Route::post('log', function()

2. {

3. return Input::get('nom');

4. });

Nous verrons en détail la récupération des entrées dans un autre chapitre, mais la clarté de la syntaxe de Laravel doit vous rendre cette écriture déjà évidente.

En réponse à l'URL : http://localhost/laravel/public/test.html nous obtenons le formulaire :

Et si nous entrons « toto » comme dans le cas de cette image, ce nom doit s'afficher comme réponse à l'écran, ce qui montre que les variables « post » sont bien prises en compte.

IV-A-3 - Les paramètres

Il est classique de transmettre des paramètres dans une requête de type « get », il faut alors pouvoir récupérer ces paramètres. Voici un exemple :

1. Route::get('article/{numero}', function($numero)

2. {

3. return "Le numéro de l'article est $numero";

4. });

Exemple d'URL : http://localhost/laravel/public/article/24.

Remarquez la syntaxe, le paramètre est placé entre accolades, on reprend son nom comme paramètre de la fonction et on n'a plus qu'à l'utiliser.

Avec cette syntaxe si vous ne mettez aucun paramètre vous obtenez un message d'erreur. Il est possible de rendre le paramètre optionnel en faisant suivre un point d'interrogation à son nom. Mais il faut prévoir une valeur par défaut pour la variable dans la fonction pour éviter un message d'erreur :

1. Route::get('article/{numero?}', function($numero = null)

2. {

3. if($numero) return "Le numéro de l'article est $numero";

4. else return 'Aucun article mentionné !';

5. });

IV-A-4 - Les contraintes

On peut contraindre une partie d'un URL à correspondre à un certain type de données avec l'utilisation d'une expression régulière. Voici un exemple :

1. Route::get('article/{numero}', function($numero)

2. {

3. return "Le numéro de l'article est $numero";

4. })->where('numero', '[0-9]+');

Ici on oblige le numéro à être un nombre.

IV-A-5 - Nommer les routes et redirections

Vous pouvez attribuer un nom à une route pour en garder une référence. Voici un exemple :

1. Route::get('log/{nom?}', array('as' => 'loger', function($nom = null)

2. {

3. if($nom) return $nom;

4. else return 'Vous devez vous connecter !';

5. }));

6. Route::get('login/{nom?}', function($nom = null)

7. {

8. return Redirect::route('loger', array($nom));

9. });

J'ai défini deux routes, la première prend en compte un nom de connexion, la seconde n'en prend pas mais elle redirige vers la première. Bon, ce n'est pas un exemple très réaliste mais il montre le fonctionnement du système.

Si vous entrez http://localhost/laravel/public/login vous obtenez « Vous devez vous connecter ! » et si vous entrez http://localhost/laravel/public/login/albert vous obtenez « albert ».

Si vous ne nommez pas la route vous pouvez quand même faire une redirection en passant l'URI avec la méthode to :

1. Route::get('log/{nom?}', function($nom = null)

2. {

3. if($nom) return $nom;

4. else return 'Vous devez vous connecter !';

5. });

6. Route::get('login/{nom?}', function($nom = null)

7. {

8. return Redirect::to('log'.'/'.$nom);

9. });

La version bêta 5 a réintroduit la commande Redirect::home() de Laravel 3 (pour que ça fonctionne il faut que la route correspondante soit nommée home).

Nous verrons d'autres cas de redirection plus tard.

IV-A-6 - Groupement de routes et préfixage

Il est possible de grouper des routes pour leur appliquer un traitement spécifique, par exemple pour tenir compte d'un préfixe (nous verrons dans un chapitre ultérieur d'autres cas d'application avec un filtrage) :

1. Route::group(array('prefix' => 'login'), function()

2. {

3. Route::get('', function($nom = null)

4. {

5. return 'Vous devez vous connecter !';

6. });

7. Route::get('{nom}', function($nom = null)

8. {

9. return 'Bienvenu '.$nom;

10. });

11. });

Le groupement de routes se fait avec la méthode Route::group. Ici j'ai aussi défini le préfixe login qui est appliqué à toutes les routes du groupe. Ainsi avec l'URL http://localhost/laravel/public/login on obtient :

Vous devez vous connecter !

Et avec l'URL http://localhost/laravel/public/login/toto on obtient :

Bienvenu toto

Nous verrons d'autres possibilités pour les routes au cours des chapitres suivants.

V - Chapitre 5 : Les entrées

Nous avons vu comment une requête est dirigée selon son URI grâce aux routes. Nous avons aussi vu comment récupérer des paramètres présents dans cet URI. Maintenant nous allons voir comment récupérer les données transmises.

V-A - Récupérer des données transmises

La façon la plus classique de transmettre des informations est certainement l'usage d'un formulaire. Voici un formulaire élémentaire avec deux entrées (je décris la classe Form dans un chapitre ultérieur) que j'ai inclus pour le moment dans la route de base :

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

2. {

3. echo Form::open(array('url' => 'log', 'method' => 'GET'));

4. echo Form::label('nom', 'Nom :');

5. echo Form::text('nom'),'<br>';

6. echo Form::label('age', 'Age :');

7. echo Form::text('age'),'<br>';

8. echo Form::submit('Submit');

9. echo Form::close();

10. });

Voici ce que ça donne dans un navigateur (bon d'accord ce n'est pas folichon, mais ce n'est pas le sujet) :

Nous allons créer une route pour intercepter l'URL de retour et prendre en compte les valeurs entrées :

1. Route::any('log', function()

2. {

3. return 'Le nom est '.Input::get('nom').'<br>'.'L\'âge est '.Input::get('age');

4. });

Vous remarquez que pour la route je n'ai utilisé ni get, ni post, mais any. Ainsi la route sera activée quelle que soit la méthode utilisée. En l'occurrence dans le formulaire j'ai prévu la méthode get. Par exemple pour les entrées toto et 28 l'URI prend cette forme :

http://localhost/laravel/public/log?nom=toto&age=28

Et on obtient en retour :

Le nom est toto
L'âge est 28

Faites le même essai avec la méthode post (comme c'est la valeur par défaut on n'est pas obligé de la mentionner) :

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

2. {

3. echo Form::open(array('url' => 'log'));

4. echo Form::label('nom', 'Nom :');

5. echo Form::text('nom'),'<br>';

6. echo Form::label('age', 'Age :');

7. echo Form::text('age'),'<br>';

8. echo Form::submit('Submit');

9. echo Form::close();

10. });

Vous obtenez le même résultat. On n'a donc pas à se soucier de la méthode utilisée, la syntaxe reste la même pour récupérer les valeurs :

Input::get('nom_de_la_valeur')

V-A-1 - Tester les données transmises et les valeurs par défaut

Que se passe-t-il si une valeur attendue est absente ? Si nous poursuivons avec notre exemple, si l'âge est absent j'aurai cet affichage :

Le nom est toto
L'âge est

Il y a une possibilité pour gérer ce problème, il faut tester la présence de la valeur :

1. Route::any('log', function()

2. {

3. $texte = 'Le nom est '.Input::get('nom');

4. if (Input::has('age')) $texte .='<br>'.'L\'âge est '.Input::get('age');

5. return $texte;

6. });

Il est possible de prévoir une valeur par défaut si l'entrée est absente :

$texte = 'Le nom est '.Input::get('nom', 'Dupont');

Attention ! Il faut vraiment que la donnée soit absente (c'est-à-dire null) pour que ça fonctionne, autrement dit un retour de formulaire avec un champ vide ne déclenchera pas cette option !

V-A-2 - Les données dans un tableau

Il est parfois pratique de récupérer toutes les données dans un tableau pour effectuer un traitement global. Voici comment faire :

1. Route::any('log', function()

2. {

3. return var_dump(Input::all());

4. });

Avec toujours le même formulaire et les mêmes entrées on obtient :

array(2) { ["nom"]=> string(4) "toto" ["age"]=> string(2) "28" }

On a la possibilité de sélectionner seulement certaines entrées pour le tableau. Enrichissons notre formulaire :

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

2. {

3. echo Form::open(array('url' => 'log'));

4. echo Form::label('nom', 'Nom :');

5. echo Form::text('nom'),'<br>';

6. echo Form::label('ville', 'Ville :');

7. echo Form::text('ville'),'<br>';

8. echo Form::label('age', 'Age :');

9. echo Form::text('age'),'<br>';

10. echo Form::submit('Submit');

11. echo Form::close();

12. });

Ce qui donne :

Maintenant on veut récupérer dans un tableau uniquement les entrées Nom et Ville :

1. Route::any('log', function()

2. {

3. return var_dump(Input::only('nom', 'ville'));

4. });

Voici ce que ça donne avec une saisie :

array(2) { ["nom"]=> string(6) "Durant" ["ville"]=> string(5) "Paris" }

On peut obtenir le même résultat en excluant l'âge :

1. Route::any('log', function()

2. {

3. return var_dump(Input::except('age'));

4. });

V-A-3 - Les fichiers

Voyons maintenant comment récupérer un fichier. Créons un formulaire élémentaire :

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

2. {

3. echo Form::open(array('url' => 'trans', 'enctype' => 'multipart/form-data'));

4. echo Form::file('fichier');

5. echo Form::submit('Envoyer le fichier');

6. echo Form::close();

7. });

Et voici la route :

1. Route::any('trans', function()

2. {

3. echo var_dump(Input::file('fichier'));

4. });

Et le résultat de l'action :

1. object(Symfony\Component\HttpFoundation\File\UploadedFile)[9]

2. private 'test' => boolean false

3. private 'originalName' => string 'routes.php' (length=10)

4. private 'mimeType' => string 'text/plain' (length=10)

5. private 'size' => int 642

6. private 'error' => int 0

Vous voyez ici que Laravel utilise un composant de Symfony .

Vous constatez aussi qu'il est facile de trouver le nom du fichier, son type MIME et sa taille puisqu'on a un objet qui contient ces éléments. On reçoit un fichier, il faut maintenant en faire quelque chose : normalement le placer dans un dossier quelque part, en effet par défaut PHP conserve un fichier téléchargé seulement jusqu'à la fin du script. Créez un dossier public/uploads :

Modifiez ainsi la route :

1. Route::any('trans', function()

2. {

3. Input::file('fichier')->move('uploads');

4. });

Utilisez le formulaire pour envoyer un fichier et regardez ensuite dans le dossier uploads :

Le fichier a bien été reçu et placé dans le bon dossier, par contre il se retrouve avec un nom différent et une extension .tmp. On peut imposer un nom et une extension :

1. Route::any('trans', function()

2. {

3. Input::file('fichier')->move('uploads', 'mon_fichier.jpg');

4. });

Résultat :

VI - Chapitre 6 : Les vues

Nous avons bien avancé dans notre découverte de Laravel 4. Nous savons gérer les URL et les entrées. Mais pour les réponses nous nous sommes pour le moment contentés de choses sommaires. Nous allons voir à présent comment créer des réponses élaborées avec les vues.

VI-A - Une vue simple

Il faut comprendre une vue dans le sens du modèle MVC. Pour respecter ce modèle une vue ne doit comporter que ce qui est nécessaire et suffisant pour afficher la réponse chez le client. Dans la structure de Laravel les vues doivent se placer dans le dossier app/views :

À la base une vue est juste du code HTML. Créez un fichier PHP avec ce code et enregistrez-le avec le nom bonjour.php dans le dossier app/views :

<h1>Bonjour !</h1>

Créez ensuite cette route :

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

2. {

3. return View::make('bonjour');

4. });

Entrez cet URL : http://localhost/laravel/public/bonjour

Vous devez obtenir cet affichage :

VI-A-1 - Des paramètres

Les vues seraient vraiment peu utiles si on ne pouvait pas leur transmettre de paramètre. Modifions notre vue pour lui ajouter une variable :

<h1>Bonjour <?php echo $nom ?>!</h1>

Il y a deux façons pour transmettre une variable, voyons la première avec cette route :

1. Route::get('bonjour/{nom}', function($nom)

2. {

3. return View::make('bonjour', array('nom' => $nom));

4. });

Résultat avec l'URL http://localhost/laravel/public/bonjour/Alfred :

Voici une seconde syntaxe pour un résultat identique :

1. Route::get('bonjour/{nom}', function($nom)

2. {

3. return View::make('bonjour')->with('nom', $nom);

4. });

VI-A-2 - Une sous-vue

On peut imbriquer une vue dans une autre. Voici par exemple une vue que nous allons appeler sous_vue.php :

<h3>Bonjour <?php echo $nom ?>!</h3>

Créons une deuxième vue que nous appelons bonjour.php :

<h1>Bienvenue !</h1>
<?php echo $enfant; ?>

Et enfin une route :

1. Route::get('bonjour/{nom}', function($nom)

2. {

3. return View::make('bonjour')->nest('enfant', 'sous_vue', array('nom' => $nom));

4. });

Entrons l'URL http://localhost/laravel/public/bonjour/Albert.

Nous obtenons :

VI-A-3 - Le composeur de vue

Imaginez maintenant que vous voulez lier des données à une vue chaque fois que vous la chargez. Évidemment vous pouvez écrire le code correspondant directement à chaque appel, mais il existe une autre possibilité basée sur la gestion d'un événement. Voici un exemple pour en comprendre le fonctionnement. Commencez par créer une première vue que vous appelez header.php :

<h1>Bienvenue !</h1>

Créez une deuxième vue que vous appelez bonjour.php :

<?php echo $head; ?>
<h3>Bonjour <?php echo $nom ?>!</h3>

Dans le fichier des routes créez ce code :

1. View::composer('bonjour', function($view)

2. {

3. $view->nest('head', 'header');

4. });

6. Route::get('bonjour/{nom}', function($nom)

7. {

8. return View::make('bonjour')->with('nom', $nom);

9. });

Maintenant si vous utilisez l'URL http://localhost/laravel/public/bonjour/Jean vous obtenez :

Vous pouvez, avec cette méthode aussi, attacher des données systématiquement à une vue chaque fois que vous la chargez. Prenons un exemple avec la vue bonjour.php :

<h3><?php echo $greeting.' '.$nom ?>!</h3>

Avec ce code dans les routes :

1. View::composer('bonjour', function($view)

2. {

3. $view->with('greeting', 'Bonjour');

4. });

6. Route::get('bonjour/{nom}', function($nom)

7. {

8. return View::make('bonjour')->with('nom', $nom);

9. });

Maintenant si vous utilisez l'URL http://localhost/laravel/public/bonjour/Jean, vous obtenez :

VII - Chapitre 7 : Un exemple de site statique

Avec tout ce que nous avons vu jusqu'à présent il est tout à fait possible de réaliser un petit moteur de site statique, et il est grand temps de mettre un peu en pratique tous ces éléments. Commençons par réaliser le template (gabarit) HTML de la page. Personnellement je commence par réaliser dans un premier temps une page HTML pour régler tous les aspects stylistiques. J'utilise Bootstrap  pour simplifier la mise en page et le style.

Cette section a été mise à jour avec Bootstrap 3

VII-A - Création de la page

Voici ce que je vous propose :

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

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

5. <title>Mon beau site</title>

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

7. <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css">

8. <link href="assets/css/main.css" rel="stylesheet" type="text/css">

9. <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->

10. <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->

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

12. <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>

13. <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>

14. <![endif]-->

15. </head>

17. <body>

18. <div class="container">

19. <header class="jumbotron">

20. <h1>Mon beau site !</h1>

21. </header>

22. <div class="navbar navbar-default">

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

24. <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">

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

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

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

28. </button>

29. <a class="navbar-brand" href="#">Mon beau site</a>

30. </div>

31. <div class="collapse navbar-collapse">

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

33. <li class="active"><a href="1">Page1</a></li>

34. <li><a href="2">Page2</a></li>

35. <li><a href="2">Page3</a></li>

36. </ul>

37. </div>

38. </div>

39. <div class="col-md-12"> Contenu de la page</div>

40. <hr>

41. <footer class="col-md-12" id="bas"> © Mon beau site... </footer>

42. </div>

43. <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>

44. <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.2/js/bootstrap.min.js"></script>

45. </body>

Pour Bootstrap et JQuery j'utilise des CDN (Content Delivery Network).

Ajoutez cette feuille de style que vous appelerez main.css et que vous placerez dans le dossier public/assets/css :

1. @charset "utf-8";

2. body {

3. padding-top: 20px;

4. min-height: 2000px;

5. color: #252;

6. }

7. body, .jumbotron, .navbar {

8. background: -moz-radial-gradient(top left, rgba(0,160,0,.5), rgba(0,240,0,0));

9. background: -webkit-radial-gradient(top left, rgba(0,0,255,.5), rgba(0,240,0,0));

10. background: -ms-radial-gradient(top left, rgba(0,160,0,.5), rgba(0,240,0,0));

11. background: -o-radial-gradient(top left, rgba(0,160,0,.5), rgba(0,240,0,0));

12. filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#8383FF', endColorstr='#D8D8FF', GradientType=0);

13. background: radial-gradient(top left, rgba(0,160,0,.5), rgba(0,240,0,0));

14. }

15. #bas {

16. text-align: center;

17. }

18. p {

19. text-align: justify;

20. }

21. .jumbotron {

22. padding: 30px;

23. border-radius:20px;

24. color:#90d;

25. text-shadow: 0 2px 0 #FFFFFF;

26. }

27. .jumbotron p {

28. text-shadow: 0 1px 0 #FFFFFF;

29. }

30. .jumbotron, .navbar {

31. box-shadow: 3px 3px 3px #059;

32. }

33. .navbar .nav > li > a {

34. color: #90d;

35. font-size: 16px;

36. }

37. .navbar .nav > .active > a, .navbar .nav > .active > a:hover, .navbar .nav > .active > a:focus {

38. background-color: #4c4;

39. box-shadow: 1px 1px 1px 1px #059;

40. }

Ce qui doit donner ce visuel :

VII-A-1 - Le template

À partir du code HTML nous allons insérer le code PHP pour rendre notre page dynamique. Essentiellement nous voulons adapter le menu et le contenu :

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

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

5. <title>Mon beau site</title>

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

7. <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css">

8. <link href="assets/css/main.css" rel="stylesheet" type="text/css">

9. <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->

10. <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->

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

12. <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>

13. <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>

14. <![endif]-->

15. </head>

17. <body>

18. <div class="container">

19. <header class="jumbotron">

20. <h1>Mon beau site !</h1>

21. </header>

22. <div class="navbar navbar-default">

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

24. <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">

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

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

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

28. </button>

29. <a class="navbar-brand" href="#">Mon beau site</a>

30. </div>

31. <div class="collapse navbar-collapse">

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

33. <?php

34. for($i = 0; $i < count($data['menu']); ++$i) {

35. echo '<li';

36. if($i == $data['page']) echo ' class="active"';

37. echo '><a href="'.$i.'">'.$data['menu'][$i].'</a></li>';

38. }

39. ?>

40. </ul>

41. </div>

42. </div>

43. <div class="col-md-12"><?php echo $contenu; ?></div>

44. <hr>

45. <footer class="col-md-12" id="bas"> © Mon beau site... </footer>

46. </div>

47. <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>

48. <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.2/js/bootstrap.min.js"></script>

49. </body>

Cette construction suppose qu'on reçoit un tableau $data qui contient un autre tableau $menu avec le nom des pages et une variable $contenu avec le contenu de la page. Enregistrez ce fichier en le nommant template dans le dossier app/views.

VII-A-2 - Les pages

Réalisons maintenant les vues qui doivent afficher le contenu des pages. Nous allons faire simple. Voici la vue app/views/page0 :

Contenu de l'accueil

Et voici pour les autres vues (app/views/page1, app/views/page2, app/views/page3) :

1. Contenu de la page 1

3. Contenu de la page 2

5. Contenu de la page 3

Vous devez donc avoir ces vues :

VII-A-3 - La route

Il ne nous reste plus qu'à écrire la route pour faire fonctionner tout ça :

1. Route::get('/{page?}', function($page = 0)

2. {

3. $data = array(

4. 'menu' => array('Accueil','Page1','Page2','Page3'),

5. 'page' => $page

6. );

7. return View::make('template')->with('data', $data)->nest('contenu', 'page'.$page);

8. })->where('page', '[0-3]');

10. App::missing(function($exception)

11. {

12. return 'Oups ! Je ne connais pas cette page !';

13. });

La syntaxe est simple et claire et ça fonctionne très bien .

On attend le numéro de page (avec 0 comme valeur par défaut), on définit les données à transmettre au template et on contraint le numéro de page avec la clause « where ».

Enfin j'ai prévu un message pour signaler que la page n'existe pas : nous n'avons pas encore vu les messages d'erreurs, mais au moins l'application est ainsi complète et fonctionnelle.

VIII - Chapitre 8 : Blade

Mis à jour avec Bootstrap 3.

Pour le moment nous avons utilisé du PHP pur dans nos vues pour générer le code HTML qui va bien. C'est une méthode simple et efficace mais certains lui reprochent sa lourdeur syntaxique. On peut aussi arguerque PHP a été conçu à la base pour ça et qu'il le fait très bien avec des performances louables. Pour ceux qui s'en contentent c'est parfait, pour les autres des moteurs de template ont été créés. Laravel n'échappe pas à la règle et propose aussi son moteur : Blade. Je vous propose de le découvrir aujourd'hui.

VIII-A - Syntaxe de base

Lorsqu'on veut insérer une donnée avec PHP on utilise cette syntaxe :

<?php echo $valeur ; ?>

On peut aussi utiliser la syntaxe courte (avec PHP 5.4 ou si short_open_tag est activé) :

<?= $valeur ?>

Avec Blade on a cette syntaxe :

{{ $valeur }}

Blade va gentiment transformer ça en :

<?php echo $valeur ; ?>

Pour « échapper » (application de htmlentities) le texte il faut utiliser la triple accolade (il y a eu de chaudes discussions sur les forums pour définir cela) :

{{{ $valeur }}}

VIII-A-1 - Les conditions

Heureusement Blade ne s'arrête pas à cette substitution. Il permet aussi de simplifier l'écriture de conditions. Prenons le cas très fréquent du foreach. Créez une vue avec ce code, nommez-la conditions :

1. <?php

2. foreach($data as $element) {

3. echo $element.'<br>';

4. }

5. ?>

Créez cette route :

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

2. {

3. $data = array('un','deux','trois','quatre');

4. return View::make('conditions')->with('data', $data);

5. });

Si vous entrez l'URL http://localhost/laravel/public/ vous devez voir s'afficher :

un
deux
trois
quatre

Voyons l'équivalent avec Blade. Renommez votre vue conditions.blade.php et entrez ce code :

@foreach ($data as $element)
{{ $element }}<br>
@endforeach

Normalement vous devez obtenir le même résultat.

Voyons d'autres syntaxes maintenant que vous avez compris le principe. Remplacez le code de la vue par celui-ci :

@for ($i =0; $i < count($data); $i++)
{{ $data[$i] }}<br>
@endfor

Le résultat est évidemment le même. Entrez maintenant ce code :

1. @foreach ($data as $element)

2. @if ($element == 'un')

3. <p>Je suis le premier et je suis {{$element}} !</p>

4. @else

5. <p>Je ne suis pas le premier et je suis {{$element}} !</p>

6. @endif

7. @endforeach

Cette fois le résultat est :

Je suis le premier et je suis un !
Je ne suis pas le premier et je suis deux !
Je ne suis pas le premier et je suis trois !
Je ne suis pas le premier et je suis quatre !

Je pense que vous avez compris le principe .

VIII-A-2 - Mise en page

Tout cela est déjà bien pratique et rend notre code plus propre. Mais Blade va encore plus loin en nous proposant de puissantes possibilités de mise en page. Prenons un exemple pour voir ça. Créez une vue app/view/mon_template.blade.php avec ce code :

1. <html>

2. <body>

3. <h1>@yield('titre')</h1>

4. @section('contenu')

5. <p>Contenu principal.</p>

6. @show

7. </body>

8. </html>

Créez une seconde vue app/view/page.blade.php avec ce code :

1. @extends('mon_template')

3. @section('titre')

4. Mon titre

5. @stop

7. @section('contenu')

8. @parent

9. <p>Contenu de la page</p>

10. @stop

Créez enfin cette route :

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

2. {

3. return View::make('page');

4. });

Si vous entrez l'URL http://localhost/laravel/public/ vous obtenez :

Je suppose que ce simple exemple vous a montré le principe. On crée un template avec du code HTML et deux catégories de champs : @yield pour réserver la place pour une information et @section pour quelque chose de plus fourni avec une information déjà présente. Attention à la syntaxe ! La section se termine par @show.

Pour utiliser le template on doit d'abord le référencer avec @extends. Ensuite on référence les zones réservées avec @section. Et cette fois une section se clôt avec @stop. On inclut le contenu parent avec @parent. On peut ainsi constituer facilement des pages complexes.

VIII-A-3 - Un exemple

Je vais prendre le même exemple que celui du fil précédent avec un site statique mais cette fois en utilisant Blade. Voilà le template que vous appelez template_page.blade.php :

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

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

5. <title>Mon beau site</title>

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

7. <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css">

8. <link href="assets/css/main.css" rel="stylesheet" type="text/css">

9. <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->

10. <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->

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

12. <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>

13. <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>

14. <![endif]-->

15. </head>

17. <body>

18. <div class="container">

19. <header class="jumbotron">

20. <h1>Mon beau site !</h1>

21. </header>

22. <div class="navbar navbar-default">

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

24. <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">

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

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

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

28. </button>

29. <a class="navbar-brand" href="#">Mon beau site</a>

30. </div>

31. <div class="collapse navbar-collapse">

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

33. @for ($i = 0; $i < count($data['menu']); $i++)

34. <li{{ ($i == $data['page'])? ' class="active"': ''; }}><a href="{{ $i }}">{{ $data['menu'][$i] }}</a></li>

35. @endfor

36. </ul>

37. </div>

38. </div>

39. <div class="col-md-12"> @yield('content') </div>

40. <hr>

41. <footer class="col-md-12" id="bas"> © Mon beau site... </footer>

42. </div>

43. <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>

44. <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.2/js/bootstrap.min.js"></script>

45. </body>

Je me suis contenté d'utiliser la syntaxe de Blade pour générer le menu et j'ai créé une section pour le contenu. Maintenant il faut modifier les pages en les nommant page0.blade.php, page1.blade.php, page2.blade.php et page3.blade.php :

page0.blade.php

1. @extends('template_page')

2. @section('content')

3. Contenu de l'accueil

4. @stop

page1.blade.php

1. @extends('template_page')

2. @section('content')

3. Contenu de la page 1

4. @stop

page2.blade.php

1. @extends('template_page')

2. @section('content')

3. Contenu de la page 2

4. @stop

page3.blade.php

1. @extends('template_page')

2. @section('content')

3. Contenu de la page 3

4. @stop

Et enfin on modifie la route :

1. Route::get('/{page?}', function($page = 0)

2. {

3. $data = array(

4. 'menu' => array('Accueil','Page1','Page2','Page3'),

5. 'page' => $page

6. );

7. return View::make('page'.$page)->with('data', $data);

8. })->where('page', '[0-3]');

10. App::missing(function($exception)

11. {

12. return 'Oups ! Je ne connais pas cette page !';

13. });

Je rappelle aussi le contenu du fichier CSS :

1. @charset "utf-8";

2. body {

3. padding-top: 20px;

4. min-height: 2000px;

5. color: #252;

6. }

7. body, .jumbotron, .navbar {

8. background: -moz-radial-gradient(top left, rgba(0,160,0,.5), rgba(0,240,0,0));

9. background: -webkit-radial-gradient(top left, rgba(0,0,255,.5), rgba(0,240,0,0));

10. background: -ms-radial-gradient(top left, rgba(0,160,0,.5), rgba(0,240,0,0));

11. background: -o-radial-gradient(top left, rgba(0,160,0,.5), rgba(0,240,0,0));

12. filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#8383FF', endColorstr='#D8D8FF', GradientType=0);

13. background: radial-gradient(top left, rgba(0,160,0,.5), rgba(0,240,0,0));

14. }

15. #bas {

16. text-align: center;

17. }

18. p {

19. text-align: justify;

20. }

21. .jumbotron {

22. padding: 30px;

23. border-radius:20px;

24. color:#90d;

25. text-shadow: 0 2px 0 #FFFFFF;

26. }

27. .jumbotron p {

28. text-shadow: 0 1px 0 #FFFFFF;

29. }

30. .jumbotron, .navbar {

31. box-shadow: 3px 3px 3px #059;

32. }

33. .navbar .nav > li > a {

34. color: #90d;

35. font-size: 16px;

36. }

37. .navbar .nav > .active > a, .navbar .nav > .active > a:hover, .navbar .nav > .active > a:focus {

38. background-color: #4c4;

39. box-shadow: 1px 1px 1px 1px #059;

40. }

La syntaxe est bien plus simple avec Blade .

IX - Chapitre 9 : Les contrôleurs

Nous avons vu que nous pouvons nous contenter de routes et de vues pour créer un site. C'est parfait tant qu'on doit faire des choses simples, mais avec une application un peu lourde on peut se retrouver avec beaucoup de code dans le fichier des routes, ce qui n'est pas très judicieux. Une autre option consiste à créer des contrôleurs (schéma MVC). Cette solution permet aussi l'injection automatique de dépendances comme nous le verrons plus tard.

IX-A - Créer un contrôleur

Si vous regardez dans le dossier app/controllers, vous trouvez le fichier HomeController.php :

1. class HomeController extends BaseController {

2. /*

3. |--------------------------------------------------------------------------

4. | Default Home Controller

5. |--------------------------------------------------------------------------

6. |

7. | You may wish to use controllers instead of, or in addition to, Closure

8. | based routes. That's great! Here is an example controller method to

9. | get you started. To route to this controller, just add the route:

10. |

11. |    Route::get('/', 'HomeController@showWelcome');

12. |

13. */

15. public function showWelcome()

16. {

17. return View::make('hello');

18. }

20. }

On vous explique que c'est un exemple pour réaliser vos propres contrôleurs. La syntaxe est simple : on crée une classe qui hérite de BaseController. Notez que le nom de la classe, conformément au PSR-1 adopte le principe du StudlyCaps, c'est-à-dire que chaque mot commence par une majuscule, y compris le premier (contrairement au camelCase). Le fichier de cette classe se trouve dans le même dossier app/controllers/BaseController.php :

1. class BaseController extends Controller {

3. /**

4. * Setup the layout used by the controller.

5. *

6. * @return void

7. */

8. protected function setupLayout()

9. {

10. if ( ! is_null($this->layout))

11. {

12. $this->layout = View::make($this->layout);

13. }

14. }

16. }

Cette classe hérite de la classe Controller qui est la classe générique de Laravel pour les contrôleurs. Vous pouvez donc placer ici du code qui concerne tous vos contrôleurs. Il y a d'ailleurs déjà une méthode pour générer un template. Nous allons pour le moment laisser de côté la classe BaseController et voir comment on active un contrôleur avec une route. Puisqu'on a déjà un contrôleur, on va l'utiliser. En plus on nous explique gentiment dans le commentaire comment on doit faire . Entrez donc ce code dans le fichier des routes :

Route::get('/', 'HomeController@showWelcome');

Si vous n'avez pas supprimé le fichier app/views/hello.php vous devez obtenir cet affichage :

Si vous l'avez supprimé il suffit de le recréer avec ce code :

<h1>Hello World!</h1>

Avec ce que nous avons vu jusqu'à présent, l'utilisation de contrôleur n'apporte pas grand-chose si ce n'est d'organiser le code différemment. Au lieu de le mettre directement dans la route on le déporte dans le contrôleur. Tout cela prendra son sens avec des fonctionnalités plus avancées que nous verrons ultérieurement.

IX-A-1 - Un contrôleur « RESTful »

Vous pouvez avec Laravel créer un contrôleur qui répond aux conventions de nommage de la norme REST. Voyons un exemple simple de contrôleur :

1. class GuestController extends BaseController {

3. /**

4. * Affiche le formulaire de login

5. *

6. * @return View

7. */

8. public function getLogin()

9. {

10. //  ...

11. }

13. /**

14. * Traitement du formulaire de login

15. *

16. * @return Redirect

17. */

18. public function postLogin()

19. {

20. //  ...

21. }

22. }

On a deux actions login (identifiant), une avec la méthode GET et l'autre la méthode POST. Au niveau des routes on se contente de cette ligne de code :

Route::controller('guest', 'GuestController');

Avec l'URL http://localhost/blog/public/guest/login on active l'action login qui correspond à la méthode getLogin. Avec le même URL mais la méthode POST on active l'action login qui correspond à la méthode postLogin. Il suffit donc de bien préfixer les actions avec get et post. Je montrerai un exemple complet d'application ultérieurement.

IX-A-2 - Créer un contrôleur de ressource avec artisan

Laravel possède un outil en ligne nommé Artisan. Il permet de faire bien des choses. Pour vous en rendre compte, entrez php artisan en ligne de commande :

Vous pouvez voir qu'il y a pas mal de commandes disponibles. Nous allons nous intéresser pour le moment à la commande controller:make qui sert justement à créer un contrôleur. Voyons les options de cette commande :

Nous allons commencer par une création simple :

On nous dit que le contrôleur a été créé, nous le trouvons effectivement dans app/controllers/mon_controleur.php :

1. <?php

3. class mon_controleur extends BaseController {

5. /**

6. * Display a listing of the resource.

7. *

8. * @return Response

9. */

10. public function index()

11. {

12. //

13. }

15. /**

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

17. *

18. * @return Response

19. */

20. public function create()

21. {

22. //

23. }

25. /**

26. * Store a newly created resource in storage.

27. *

28. * @return Response

29. */

30. public function store()

31. {

32. //

33. }

35. /**

36. * Display the specified resource.

37. *

38. * @return Response

39. */

40. public function show($id)

41. {

42. //

43. }

45. /**

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

47. *

48. * @return Response

49. */

50. public function edit($id)

51. {

52. //

53. }

55. /**

56. * Update the specified resource in storage.

57. *

58. * @return Response

59. */

60. public function update($id)

61. {

62. //

63. }

65. /**

66. * Remove the specified resource from storage.

67. *

68. * @return Response

69. */

70. public function destroy($id)

71. {

72. //

73. }

75. }

Laravel se conforme à l'architecture RESTful ce qui explique la présence de toutes ces méthodes. Pour clarifier un peu tout ça, voilà le code modifié avec un retour évocateur :

1. <?php

3. class mon_controleur extends BaseController {

5. public function index()

6. {

7. return "Réponse de base";

8. }

10. // GET /ma_ressource/create

11. public function create()

12. {

13. return "La ressource doit être créée ici";

14. }

16. // POST /ma_ressource

17. public function store()

18. {

19. return "Ressource mémorisée";

20. }

22. // GET /ma_ressource/{id}

23. public function show($id)

24. {

25. return "C'est ici qu'on montre la ressource ".$id;

26. }

28. // GET /ma_ressource/{id}/edit

29. public function edit($id)

30. {

31. return "C'est ici qu'on modifie la ressource ".$id;

32. }

34. // PUT /ma_ressource/{id}

35. public function update($id)

36. {

37. return "C'est ici qu'on met à jour la ressource ".$id;

38. }

40. // DELETE /ma_ressource/{id}

41. public function destroy($id)

42. {

43. return "C'est ici qu'on supprime la ressource ".$id;

44. }

46. }

On va aussi déclarer la ressource dans le fichier des routes, ce qui va avoir pour effet de créer toutes les routes correspondant à cette ressource avec une seule ligne de code :

Route::resource('ma_ressource', 'mon_controleur');

En réponse à http://localhost/laravel/public/ma_ressource j'obtiens donc :

Réponse de base

En réponse à http://localhost/laravel/public/ma_ressource/toto j'obtiens donc :

C'est ici qu'on montre la ressource toto

En réponse à http://localhost/laravel/public/ma_ressource/toto/edit j'obtiens donc :

C'est ici qu'on modifie la ressource toto

Pour les autres commandes il faut un peu plus d'intendance puisqu'on a les verbes POST, PUT et DELETE.

Par exemple pour l'enregistrement d'une ressource créez ce formulaire :

<form action="ma_ressource" method="post">
<input name="Valider" type="submit">
</form>

Quand vous cliquez sur le bouton vous devez obtenir :

Ressource mémorisée

En ce qui concerne les méthodes PUT et DELETE la mise en œuvre est un peu plus délicate.

Si vous ne voulez pas toutes les fonctionnalités vous pouvez créer un contrôleur limité à vos besoins. La commande d'Artisan comprend les options -only et -except. Mais il faut aussi le signaler dans l'appel de la ressource. Par exemple si vous ne voulez que show et edit :

Route::resource('ma_ressource', 'mon_controleur',
array('only' => array('show', 'edit')));

Vous avez donc la possibilité d'utiliser ce type de contrôleur dans vos applications si vous voulez coller à l'architecture REST ou tout simplement partir sur une base cohérente.

Je donne un exemple complet d'une ressource avec un générateur plus loin dans ce tutoriel.

X - Chapitre 10 : Les filtres

Nous allons maintenant voir un fichier que nous n'avons pas encore ouvert : filters.php. Avec son nom vous devez déjà vous douter de son utilité, il va effectivement nous servir à effectuer des filtrages. À quoi servent les filtres ? Tout simplement à créer au niveau des routes un tri pour ne laisser passer que les informations utiles. Nous verrons aussi plus tard que c'est le lieu idéal pour vérifier qu'un utilisateur est authentifié. Si vous ouvrez le fichier filters.php vous trouvez déjà du code :

1. <?php

3. /*

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

5. | Application & Route Filters

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

7. |

8. | Below you will find the "before" and "after" events for the application

9. | which may be used to do any work before or after a request into your

10. | application. Here you may also register your custom route filters.

11. |

12. */

14. App::before(function($request)

15. {

16. //

17. });

20. App::after(function($request, $response)

21. {

22. //

23. });

25. /*

26. |--------------------------------------------------------------------------

27. | Authentication Filters

28. |--------------------------------------------------------------------------

29. |

30. | The following filters are used to verify that the user of the current

31. | session is logged into this application. The "basic" filter easily

32. | integrates HTTP Basic authentication for quick, simple checking.

33. |

34. */

36. Route::filter('auth', function()

37. {

38. if (Auth::guest()) return Redirect::guest('login');

39. });

42. Route::filter('auth.basic', function()

43. {

44. return Auth::basic();

45. });

47. /*

48. |--------------------------------------------------------------------------

49. | Guest Filter

50. |--------------------------------------------------------------------------

51. |

52. | The "guest" filter is the counterpart of the authentication filters as

53. | it simply checks that the current user is not logged in. A redirect

54. | response will be issued if they are, which you may freely change.

55. |

56. */

58. Route::filter('guest', function()

59. {

60. if (Auth::check()) return Redirect::to('/');

61. });

63. /*

64. |--------------------------------------------------------------------------

65. | CSRF Protection Filter

66. |--------------------------------------------------------------------------

67. |

68. | The CSRF filter is responsible for protecting your application against

69. | cross-site request forgery attacks. If this special token in a user

70. | session does not match the one given in this request, we'll bail.

71. |

72. */

74. Route::filter('csrf', function()

75. {

76. if (Session::token() != Input::get('_token'))

77. {

78. throw new Illuminate\Session\TokenMismatchException;

79. }

80. });

Nous avons en premier des filtres pour l'application, c'est-à-dire qu'ici nous pouvons traiter les requêtes avant même qu'elles ne soient triées par les routes avec App::before et de même nous pouvons traiter la réponse finale avec App::after. Testons déjà cela, entrez ce code :

1. App::before(function($request)

2. {

3. return 'La requête est '.$request;

4. });

Ensuite tapez n'importe quelle requête, par exemple http://localhost/laravel/public. Voilà ce que j'obtiens :

1. La requête est GET /laravel/public/ HTTP/1.1 Accept: text/html,

2. application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

3. Accept-Encoding: gzip, deflate Accept-Language: fr,fr-fr;

4. q=0.8,en-us;q=0.5,en;q=0.3 Connection: keep-alive

5. Host: localhost Referer: http://localhost/laravel/

6. User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:25.0)

7. Gecko/20100101 Firefox/25.0

J'ai bien le contenu complet de la requête. On pourrait tester de la même façon la réponse, mais vous avez compris le principe. Supprimez le code que nous avons entré sinon plus rien ne marchera .

X-A - Notre premier filtre

Maintenant nous allons voir comment écrire des filtres. Entrez ce code juste à la suite des filtres de l'application dans le fichier des filtres :

1. Route::filter('big_article', function()

2. {

3. if (Input::get('id_article') > 100)

4. {

5. return 'Traitement d\'un article avec un id supérieur à 100';

6. }

7. });

Nous avons créé un filtre sur la variable id_article, si celle-ci est supérieure à 100 on effectue un traitement particulier. Maintenant nous allons utiliser ce filtre dans une route :

1. Route::get('articles', array('before' => 'big_article', function()

2. {

3. return 'Traitement d\'un article avec un id inférieur ou égal à 100';

4. }));

Et finalement un formulaire pour faire fonctionner tout ça :

1. <form action="articles" method="get">

2. <input name="id_article" type="text" value="10">

3. <input name="Valider" type="submit">

4. </form>

Nous obtenons pour une valeur inférieure à 101 :

Traitement d'un article avec un id inférieur ou égal à 100

Et pour une valeur supérieure :

Traitement d'un article avec un id supérieur à 100

X-A-1 - Appliquer plusieurs filtres

Nous ne sommes pas limités à un filtre par route, nous allons compléter notre exemple. Ajoutez ce filtre :

1. Route::filter('danger', function()

2. {

3. if (Input::get('nom_article') == 'arme')

4. {

5. return 'Nous ne vendons pas ce genre de chose !';

6. }

7. });

Modifiez ainsi la route :

1. Route::get('articles', array('before' => 'danger|big_article', function()

2. {

3. return 'Traitement d\'un article avec un id inférieur ou égal à 100';

4. }));

Et complétez ainsi le formulaire :

1. <form action="articles" method="get">

2. <input name="id_article" type="text" value="10">

3. <input name="nom_article" type="text" value="chemise"><br>

4. <input name="Valider" type="submit">

5. </form>

Maintenant vous avez un filtrage sur deux entrées.

X-A-2 - Appliquer un filtre à plusieurs routes

On peut aussi appliquer un (ou plusieurs) filtre(s) à plusieurs routes. Pour montrer ça, on garde le filtre sur l'id_article et on crée ces routes :

1. Route::when('articles*', 'big_article');

2. Route::get('articles1', function()

3. {

4. return 'Traitement d\'un article avec un id inférieur ou égal à 100';

5. });

6. Route::get('articles2', function()

7. {

8. return 'Traitement d\'un article avec un id inférieur ou égal à 100';

9. });

Testez avec ces deux formulaires :

1. <form action="articles1" method="get">

2. <input name="id_article" type="text" value="10"><br>

3. <input name="Valider" type="submit">

4. </form>

1. <form action="articles2" method="get">

2. <input name="id_article" type="text" value="10"><br>

3. <input name="Valider" type="submit">

4. </form>

Vous devriez avoir le même résultat.

X-A-3 - Filtres sur les contrôleurs

Il est aussi évidemment possible d'appliquer des filtres à des contrôleurs. On peut le faire de façon similaire à ce que nous avons vu pour les routes. Modifiez ainsi le code du contrôleur HomeController :

1. class HomeController extends BaseController {

3. public function articles()

4. {

5. return 'Traitement d\'un article avec un id inférieur ou égal à 100';

6. }

7. }

Créez cette route :

Route::get('articles', array('before' => 'big_article', 'uses' => 'HomeController@articles'));

Et utilisez ce formulaire :

1. <form action="articles" method="get">

2. <input name="id_article" type="text" value="10"><br>

3. <input name="Valider" type="submit">

4. </form>

Vous aurez le même résultat que pour les routes vues ci-dessus.

On peut aussi déclarer le filtre dans le constructeur du contrôleur :

1. class HomeController extends BaseController {

3. public function __construct()

4. {

5. $this->beforeFilter('big_article');

6. }

8. public function articles()

9. {

10. return 'Traitement d\'un article avec un id inférieur ou égal à 100';

11. }

12. }

Et se contenter alors d'une route simple :

Route::get('articles', 'HomeController@articles');

Le résultat sera le même. Enfin vous pouvez aussi écrire la logique du filtre directement dans le contrôleur :

1. class HomeController extends BaseController {

3. public function __construct()

4. {

5. $this->beforeFilter(function()

6. {

7. if (Input::get('id_article') > 100)

8. {

9. return 'Traitement d\'un article avec un id supérieur à 100';

10. }

11. });

12. }

14. public function articles()

15. {

16. return 'Traitement d\'un article avec un id inférieur ou égal à 100';

17. }

18. }

XI - Chapitre 11 : Gestion des erreurs

Un code sans erreurs c'est rare, des requêtes parfaites encore plus. Laravel affiche une page détaillée des erreurs rencontrées. Voilà par exemple ce qu'on obtient pour une page qui n'existe pas :

C'est évidemment bien pratique en phase de développement mais fortement déconseillé sur un site en production. Pourquoi ? Déjà ce n'est pas très esthétique pour l'utilisateur, et en plus ça peut guider une personne mal intentionnée pour pirater votre site. Regardez ce code dans le fichier app/config/app.php :

'debug' => true,

Changez « true » pour « false », maintenant pour une page non trouvée on obtient seulement :

C'est plus concis et largement suffisant pour l'utilisateur (bon s'il est anglophone dans ce cas mais nous avons déjà vu comment arranger ça précédemment). Donc pensez bien à enlever ce mode « debug » lorsque votre site est en production ! Maintenant ouvrez le fichier app/start/global.php. Vous trouvez ce code :

1. App::error(function(Exception $exception, $code)

2. {

3. Log::error($exception);

4. });

Je rappelle que ce fichier est lu pour chaque requête reçue lorsque vous êtes dans un environnement « global » qui est l'environnement par défaut. Nous reparlerons de cela plus tard pour nous concentrer sur les erreurs. Nous avons celles qui sont propres à PHP et celles qui sont générées par les différents composants utilisés, comme le NotFoundHttpException que nous rencontrons pour une page qui n'existe pas. Le code présent dans le fichier app/start/global.php intercepte toutes les erreurs rencontrées et affiche une information sur cette erreur. C'est a priori largement suffisant, mais on peut aussi trier les erreurs par catégories et les intercepter. Par exemple, entrez cette route avec une belle erreur PHP :

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

2. {

3. return substr(548, 5, 4);

4. });

Maintenant ajoutez ce code dans le fichier app/start/global.php :

1. App::error(function(UnexpectedValueException $exception)

2. {

3. return 'Une valeur inattendue est présente !';

4. });

Maintenant si vous utilisez l'URL http://localhost/laravel/public vous obtenez :

Une valeur inattendue est présente !

Vous avez donc le loisir de gérer les erreurs comme vous le voulez.

XI-A - Le fichier de « log »

Par défaut Laravel génère un fichier de « log » journalier dans le dossier app/storage/logs. Le nom des fichiers est de la forme log-année-mois-jour.txt, par exemple log-2013-01-22.txt. Que contient ce fichier ? Tout ce qu'on lui dit de mémoriser. Nous avons vu ci-dessus dans l'interception des erreurs la ligne :

Log::error($exception);

Par défaut on mémorise donc toutes les erreurs rencontrées. Laravel utilise Monolog. On peut générer des enregistrements personnalisés, prenons le cas ci-dessus en changeant le code :

1. App::error(function(UnexpectedValueException $exception)

2. {

3. Log::error('Je me suis encore trompé avec mes valeurs...');

4. });

Si vous utilisez l'URL http://localhost/laravel/public vous obtenez maintenant le message d'erreur classique avec tous les détails. Pour voir si notre inscription a agi il faut ouvrir le fichier de log. Je trouve cette ligne :

[2013-01-22 12:52:16] log.ERROR: Je me suis encore trompé avec mes valeurs…

Je trouve aussi l'inscription de base :

[2013-01-22 12:52:16] log.ERROR: exception ‘UnexpectedValueException' with message ‘The Response content must be a string or object implementing __toString(), « boolean » given.' in…

Monolog respecte la norme PSR-3 qui définit l'interface de log. En particulier sont définis 8 niveaux : debug, info, notice, warning, error, critical, alert, emergency. Si vous voulez utiliser des fonctionnalités de Monolog vous pouvez récupérer facilement une référence. Par exemple le code suivant est l'équivalent du précédent :

1. App::error(function(UnexpectedValueException $exception)

2. {

3. $log = Log::getMonolog();

4. $log->addError('Je me suis encore trompé avec mes valeurs...');

5. });

XI-A-1 - Les erreurs HTTP

Les erreurs HTTP peuvent être une page inconnue (404), un accès non autorisé (401),  une syntaxe erronée (400)… Il est possible de générer une de ces erreurs avec cette syntaxe :

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

2. {

3. App::abort(400, 'Syntaxe erronée !');

4. });

Maintenant si vous utilisez l'URL http://localhost/laravel/public vous obtenez :

Je rappelle aussi comment on intercepte une erreur 404 :

1. App::missing(function($exception)

2. {

3. return 'Cette page n\'existe pas !';

4. });

XII - Chapitre 12 : Les formulaires

XII-A - Ouvrir et fermer un formulaire

Commençons par quelque chose de simple : ouvrir et fermer un formulaire. Entrez ce code dans le fichier app/routes.php :

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

2. echo Form::open(array('url' => 'test'));

3. echo Form::close();

4. });

Je pars toujours du principe que j'ai un serveur en localhost dans un dossier laravel. Si j'utilise l'URL http://localhost/laravel/public je vais avoir en retour une page vide mais avec ce code HTML :

<form method="POST" action="http://localhost/laravel/public/test" accept-charset="UTF-8"></form>

On constate que l'on a par défaut la méthode POST. On voit que l'URL de retour se construit tout seul. On a aussi par défaut le charset en UTF-8.

Si on désire une autre méthode que POST il faut le préciser, par exemple pour GET :

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

2. echo Form::open(array('url' => 'test', 'method' => 'GET'));

3. echo Form::close();

4. });

Avec comme résultat :

<form method="GET" action="http://localhost/laravel/public/test" accept-charset="UTF-8"></form>

On peut aussi préciser une méthode moins courante comme PUT ou DELETE :

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

2. echo Form::open(array('url' => 'test', 'method' => 'PUT'));

3. echo Form::close();

4. });

Comme les navigateurs ignorent ces méthodes, Laravel génère un champ caché pour signaler la méthode :

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

XII-A-1 - Protection CSRF

Pour tout formulaire qui n'utilise pas GET, un champ caché est ajouté automatiquement pour la sécurité CSRF :

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

2. echo Form::open(array('url' => 'test'));

3. echo Form::close();

4. });

Dans le code généré apparaît un champ caché pour mémoriser la clé de sécurité :

<form method="POST" action="http://localhost/laravel/public/test" accept-charset="UTF-8">
<input name="_token" type="hidden" value="i0SN6gBv5W9UVYENJZGi6A9A7BB3MySAWyM4SZFt">
</form>

Au retour il suffit de tester cette clé en créant un filtre :

Route::any('test', array('before' => 'csrf', function() {

}));

XII-A-2 - Les champs texte

Voyons maintenant comment créer des champs de type « texte ». Voici un formulaire avec les cinq types disponibles :

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

2. echo Form::open(array('url' => 'test'));

3. echo Form::text('text', 'zone de texte');

4. echo Form::password('password');

5. echo Form::email('email');

6. echo Form::hidden('hidden');

7. echo Form::textarea('textarea');

8. echo Form::close();

9. });

Résultat :

1. <form method="POST" action="http://localhost/laravel/public/test" accept-charset="UTF-8">

2. <input name="text" type="text" value="zone de texte">

3. <input name="password" type="password" value="">

4. <input name="email" type="email">

5. <input name="hidden" type="hidden">

6. <textarea name="textarea" cols="50" rows="10"></textarea>

7. </form>

On peut prévoir une valeur par défaut, comme je l'ai fait pour le champ text.

XII-A-3 - Les nouveaux types du HTML 5

Le HTML 5 a introduit de nouveaux types : color, date, month… Pour les obtenir il faut utiliser la méthode input. Voici un exemple avec quelques types :

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

2. echo Form::open(array('url' => 'test'));

3. echo Form::input('color', 'color');

4. echo Form::input('date', 'date');

5. echo Form::input('month', 'month');

6. echo Form::input('number', 'number');

7. echo Form::close();

8. });

Résultat :

1. <form method="POST" action="http://localhost/laravel/public/test" accept-charset="UTF-8">

2. <input name="color" type="color">

3. <input name="date" type="date">

4. <input name="month" type="month">

5. <input name="number" type="number">

6. </form>

XII-A-4 - Les légendes

Voici comment générer une légende :

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

2. echo Form::open(array('url' => 'test'));

3. echo Form::label('nom', 'Nom : ');

4. echo Form::text('nom');

5. echo Form::close();

6. });

Code généré :

1. <form method="POST" action="http://localhost/laravel/public/test" accept-charset="UTF-8">

2. <label for="nom">Nom : </label>

3. <input name="nom" type="text" id="nom">

4. </form>

Vous remarquez que la liaison entre le champ et la légende se fait automatiquement.

XII-A-5 - Les boutons radio

Créer des boutons radio est aussi très facile :

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

2. echo Form::open(array('url' => 'test'));

3. echo Form::label('reponse', 'oui');

4. echo Form::radio('reponse', 'oui', true);

5. echo Form::label('reponse', 'non');

6. echo Form::radio('reponse', 'non');

7. echo Form::close();

8. });

Avec ce code généré :

1. <form method="POST" action="http://localhost/laravel/public/test" accept-charset="UTF-8">

2. <label for="reponse">oui</label>

3. <input checked="checked" name="reponse" type="radio" value="oui" id="reponse">

4. <label for="reponse">non</label>

5. <input name="reponse" type="radio" value="non" id="reponse">

6. </form>

Mais on constate un défaut, on se retrouve avec deux id identiques et des légendes non différenciées. Cela est dû au mode de génération. Lorsqu'un champ suit un label et s'il a le même nom que ce label, alors il se retrouve avec un id de ce même nom. C'est parfait pour un champ isolé de type texte, mais ça ne convient plus pour des champs groupés comme les boutons radio. Alors il faut un peu ruser avec le générateur :

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

2. echo Form::open(array('url' => 'test'));

3. echo Form::label('oui', 'oui');

4. echo Form::radio('reponse', 'oui', true, array('id' => 'oui'));

5. echo Form::label('non', 'non');

6. echo Form::radio('reponse', 'non', false, array('id' => 'non'));

7. echo Form::close();

8. });

On se retrouve alors avec un code correct :

1. <form method="POST" action="http://localhost/laravel/public/test" accept-charset="UTF-8">

2. <label for="oui">oui</label>

3. <input id="oui" checked="checked" name="reponse" type="radio" value="oui">

4. <label for="non">non</label>

5. <input id="non" name="reponse" type="radio" value="non">

6. </form>

XII-A-6 - Les cases à cocher

Les cases à cocher fonctionnent sur le même principe que les boutons radio, alors voici l'exemple précédent, mais cette fois avec des cases à cocher :

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

2. echo Form::open(array('url' => 'test'));

3. echo Form::label('oui', 'oui');

4. echo Form::checkbox('reponse', 'oui', true, array('id' => 'oui'));

5. echo Form::label('non', 'non');

6. echo Form::checkbox('reponse', 'non', false, array('id' => 'non'));

7. echo Form::close();

8. });

Avec ce code généré :

1. <form method="POST" action="http://localhost/laravel/public/test" accept-charset="UTF-8">

2. <label for="oui">oui</label>

3. <input id="oui" checked="checked" name="reponse" type="checkbox" value="oui">

4. <label for="non">non</label>

5. <input id="non" name="reponse" type="checkbox" value="non">

6. </form>

XII-A-7 - Les listes de choix

Vous pouvez aussi générer une liste de choix :

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

2. echo Form::open(array('url' => 'test'));

3. echo Form::select('Animaux',

4. array(

5. 'C' => 'Chimpanzé',

6. 'E' => 'Eléphant',

7. 'G' => 'Girafe',

8. 'S' => 'Serpent',

9. 'Z' => 'Zèbre')

10. );

11. echo Form::close();

12. });

Code généré :

1. <form method="POST" action="http://localhost/laravel/public/test" accept-charset="UTF-8">

2. <select name="Animaux">

3. <option value="C">Chimpanzé</option>

4. <option value="E">Eléphant</option>

5. <option value="G">Girafe</option>

6. <option value="S">Serpent</option>

7. <option value="Z">Zèbre</option>

8. </select>

9. </form>

Par défaut on a une liste déroulante sans option sélectionnée. Il est possible de rendre la liste non déroulante en précisant son attribut size, et permettre une sélection multiple avec l'attribut multiple. On peut aussi préciser une option sélectionnée :

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

2. echo Form::open(array('url' => 'test'));

3. echo Form::select('Animaux',

4. array(

5. 'C' => 'Chimpanzé',

6. 'E' => 'Eléphant',

7. 'G' => 'Girafe',

8. 'S' => 'Serpent',

9. 'Z' => 'Zèbre'),

10. 'E',

11. array('size' => 4, 'multiple' => 'multiple')

12. );

13. echo Form::close();

14. });

Résultat :

1. <form method="POST" action="http://localhost/laravel/public/test" accept-charset="UTF-8">

2. <select size="4" multiple="multiple" name="Animaux">

3. <option value="C">Chimpanzé</option>

4. <option value="E" selected="selected">Eléphant</option>

5. <option value="G">Girafe</option>

6. <option value="S">Serpent</option>

7. <option value="Z">Zèbre</option>

8. </select>

9. </form>

XII-A-8 - Les boutons

Créer un bouton est simple :

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

2. echo Form::open(array('url' => 'test'));

3. echo Form::button('Bouton');

4. echo Form::close();

5. });

Code généré :

<form method="POST" action="http://localhost/laravel/public/test" accept-charset="UTF-8">
<button type="button">Bouton</button>
</form>

Pour un bouton de soumission de formulaire, la syntaxe est :

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

2. echo Form::open(array('url' => 'test'));

3. echo Form::submit('Envoyer !');

4. echo Form::close();

5. });

Code généré :

<form method="POST" action="http://localhost/laravel/public/test" accept-charset="UTF-8">
<input type="submit" value="Envoyer !">
</form>

XII-A-9 - Pointer sur une route nommée

Voici comment faire pointer un formulaire sur une route nommée :

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

2. echo Form::open(array('route' => 'test'));

3. echo Form::submit('Envoyer !');

4. echo Form::close();

5. });

6. Route::any('test', array('as' => 'test', function() {

7. return 'Route "test"';

8. }));

XII-A-10 - Créer des macros

Si les possibilités de la classe Form ne vous suffisent pas, vous pouvez créer vos propres macros. Par exemple si vous utilisez le framework CSS Bootstrap vous pouvez trouver pratique ce genre de macro :

1. Form::macro('group', function($name, $title, $type) {

2. return '<div class="control-group">'

3. .Form::label($name, $title, array('class' => 'control-label'))

4. .'<div class="controls">'

5. .(($type == 'text')? Form::text($name): Form::password($name))

6. .'</div></div>';

7. });

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

10. echo Form::open(array('route' => 'test'));

11. echo Form::group('name', 'Nom :', 'text');

12. echo Form::group('password', 'Mot de passe :', 'password');

13. echo Form::close();

14. });

Ça a le mérite de clarifier le code pour construire le formulaire.

Code généré :

1. <form method="POST" action="http://localhost/laravel/public/test" accept-charset="UTF-8">

2. <div class="control-group">

3. <label for="name" class="control-label">Nom :</label>

4. <div class="controls">

5. <input name="name" type="text" id="name">

6. </div>

7. </div>

8. <div class="control-group">

9. <label for="password" class="control-label">Mot de passe :</label>

10. <div class="controls">

11. <input name="password" type="password" value="" id="password">

12. </div>

13. </div>

14. </form>

Il y a un paquetage intéressant pour Bootstrap.

XII-A-11 - Renseigner les champs d'un formulaire

Il arrive souvent d'avoir une erreur de validation et de représenter à l'utilisateur un formulaire et il est alors judicieux de renseigner certains champs pour éviter une nouvelle saisie. On peut le faire simplement avec une session flash :

1. Route::get('/', array('as' => 'log', function() {

2. echo Form::open(array('route' => 'test'));

3. echo Form::label('email', 'Email : ');

4. echo Form::email('email');

5. echo Form::submit('Envoyer');

6. echo Form::close();

7. }));

8. Route::any('test', array('as' => 'test', function() {

9. return Redirect::route('log')->withInput();

10. }));

La redirection s'effectue avec la valeur saisie et le champ est à nouveau rempli.

Supposons maintenant que vous voulez remplir automatiquement les champs d'un formulaire à partir d'un objet. Regardez cet exemple :

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

2. $user = new User();

3. $user->email = 'moi@chezmoi.fr';

4. echo Form::model($user, array('route' => 'test'));

5. echo Form::label('email', 'Email : ');

6. echo Form::email('email');

7. echo Form::submit('Envoyer');

8. echo Form::close();

9. });

Le champ email est automatiquement rempli à partir de la propriété de l'objet $user !

XIII - Chapitre 13 : Les bases de données 1/3

Nous allons à présent aborder un aspect important avec la gestion des bases de données. Laravel 4 est bien équipé dans ce domaine, comme nous allons le voir. Vous pouvez accéder à des bases MySQL, SQLite, SQL server et PostgreSQL.

XIII-A - Configuration

La configuration est simple et se passe dans le fichier app/config/database.php. Vous précisez le type de base :

'default' => 'mysql',

Par défaut on a MySQL, mais vous pouvez aussi mettre SQLite, PgSQL ou SQLSrv. Ensuite vous précisez les paramètres. Je vais prendre le cas le plus classique avec MySQL :

1. 'mysql' => array(

2. 'driver'    => 'mysql',

3. 'host'      => 'localhost',

4. 'database'  => 'laravel',

5. 'username'  => 'root',

6. 'password'  => '',

7. 'charset'   => 'utf8',

8. 'collation' => 'utf8_unicode_ci',

9. 'prefix'    => '',

10. ),

J'ai défini une base nommée laravel pour les tests. Laravel 4 sait faire beaucoup de choses, mais pas créer des bases : vous devez donc les créer vous-même par exemple avec PHPMyAdmin.

XIII-A-1 - Migration

Laravel possède un outil bien pratique pour gérer la structure d'une base de données : Migration. C'est un outil en ligne de commande qui fait partie des commandes d'Artisan que nous avons déjà précédemment vu. Nous allons l'utiliser pour créer une table dans notre base. La première chose à faire est de créer une migration. Qu'est-ce que c'est ? Tout simplement un peu de code qui va nous permettre d'intervenir sur la structure de la base, de faire des modifications et de pouvoir revenir en arrière si besoin. Voyons ce que nous permet de faire cet outil :

Et plus précisément ses paramètres utilisables :

Commençons par créer une migration :

Bon apparemment tout s'est bien passé, mais où est-elle cette migration ? Regardez ici :

Un fichier a été créé avec un nom composé d'un timestamp (horodateur) et du nom que nous avons donné à la migration. De cette façon il est peu probable de se retrouver avec un conflit de nommage en cas de travail en équipe. Voyons ce fichier :

1. <?php

3. use Illuminate\Database\Schema\Blueprint;

4. use Illuminate\Database\Migrations\Migration;

6. class CreateEssences extends Migration {

8. /**

9. * Run the migrations.

10. *

11. * @return void

12. */

13. public function up()

14. {

15. //

16. }

18. /**

19. * Reverse the migrations.

20. *

21. * @return void

22. */

23. public function down()

24. {

25. //

26. }

28. }

On y trouve une classe CreateEssences qui hérite de la classe Migration. On trouve aussi deux méthodes publiques up et down. C'est à nous de créer le code de ces méthodes. Dans up nous devons mettre le code qui va créer une modification dans la structure de la base, et dans down le code qui va faire l'inverse.

XIII-A-2 - Le Schema builder

Pour créer le code de migration nous allons être aidés par la classe Schema d'Illuminate. Cette classe permet de créer et de supprimer facilement des tables, des colonnes et des index. Voyons ça de plus près avec un exemple simple :

1. public function up()

2. {

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

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

5. $table->string('nom_vulgaire', 100);

6. $table->string('nom_latin', 100);

7. $table->text('description');

8. });

9. }

La syntaxe est si simple qu'elle se passe presque de commentaires. La méthode create permet de créer une table. On transmet le nom de la table à une fonction anonyme qui permet de définir les colonnes. Vous pouvez trouver toutes les possibilités de types de colonnes dans la documentation. Notez que Eloquent a besoin que chaque table ait une clé primaire numérique nommée id pour bien fonctionner (ce n'est pas indispensable mais ça simplifie la syntaxe). Maintenant nous allons activer la migration pour créer notre table :

On nous dit que la migration s'est bien effectuée. Allons voir ça. Nous trouvons deux tables dans notre base : essences et migrations. La première est celle que nous avons créée :

Elle comporte exactement ce que nous avons prévu pour elle. La seconde sert à la gestion des migrations. Maintenant nous devons nous occuper de l'action inverse de notre migration, autrement dit la suppression de la table :

1. public function down()

2. {

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

4. }

Il ne reste plus qu'à commander le rollback (retour en arrière):

Et vous pouvez vérifier que la table a bien été supprimée. Comme nous avons besoin de la table pour la suite, faites à nouveau une migration pour la recréer.

Vous pouvez ajouter 2 options (-table= et -create) dans votre migration pour obtenir une architecture de code plus complète :

Vous obtenez ce code :

1. <?php

3. use Illuminate\Database\Schema\Blueprint;

4. use Illuminate\Database\Migrations\Migration;

6. class CreateEssencesTable extends Migration {

8. /**

9. * Run the migrations.

10. *

11. * @return void

12. */

13. public function up()

14. {

15. Schema::create('essences', function(Blueprint $table)

16. {

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

18. $table->timestamps();

19. });

20. }

22. /**

23. * Reverse the migrations.

24. *

25. * @return void

26. */

27. public function down()

28. {

29. Schema::drop('essences');

30. }

32. }

XIII-A-3 - Des données d'exemple

Il existe aussi un moyen simple pour générer des données d'exemple. Créez un fichier app/database/seeds/EssenceTableSeeder.php avec ce code (nous verrons en détail la syntaxe pour la modification des données dans une table) :

1. class EssenceTableSeeder extends Seeder {

3. public function run()

4. {

5. DB::table('essences')->insert(

7. array(

8. array(

9. 'nom_vulgaire' => 'Basilic exotique',

10. 'nom_latin' => 'Ocimum basilicum ssp basilicum',

11. 'description' => 'Cette huile essentielle est antibactérienne...'

12. ),

14. array(

15. 'nom_vulgaire' => 'Bergamotier',

16. 'nom_latin' => 'Citrus bergamia',

17. 'description' => 'Cette huile essentielle est...'

18. )

19. )

21. );

22. }

24. }

Il faut aussi modifier le fichier app/database/seeds/DatabaseSeeder.php pour appeler votre classe :

1. class DatabaseSeeder extends Seeder {

3. public function run()

4. {

5. $this->call('EssenceTableSeeder');

6. }

8. }

Lancez ensuite cette commande de migration :

Allez voir dans votre base le résultat :

On peut se poser une question : pourquoi ne pas faire cette opération dans la migration ? On pourrait effectivement le faire et ça fonctionnerait, mais il est bon de bien séparer les fonctionnalités. La migration a pour but d'intervenir au niveau de la structure de la base alors que les seeds se contentent d'en modifier le contenu. Vous pourrez me dire aussi que le contenu sera renseigné au fil du temps à partir d'autres interfaces (administration, formulaire…), mais il y a parfois des données qui doivent être renseignées à la création d'un site : une liste de villes, des codes postaux, un compte administrateur, des catégories… Toutes ces données initiales se trouveront ici à leur place.

XIII-A-4 - Les modifications ultérieures

Et la suite ? Imaginez que vous voulez faire une modification dans une de vos tables, devez-vous faire un rollback, modifier le schéma, et faire une nouvelle migration ? Bon vous aboutirez bien sûr au résultat, mais si vous avez déjà entré des données vous n'allez pas apprécier de les perdre dans la manœuvre . Il est possible de faire une nouvelle migration en effectuant juste la modification nécessaire. Par exemple vous voulez rendre unique la ligne du nom latin :

1. public function up()

2. {

3. Schema::table('essences', function($table) {

4. $table->unique('nom_latin', 'essences_nom_latin_unique');

5. });

6. }

Sans oublier la manœuvre inverse :

1. public function down()

2. {

3. Schema::table('authors', function($table) {

4. $table->dropUnique('essences_nom_latin_unique');

5. });

6. }

XIII-A-5 - Scénario de migrations

Comme le processus de migration n'est pas toujours bien perçu je vous propose un petit scénario pour passer en revue les commandes disponibles. Partons d'une installation vierge de Laravel.

XIII-A-5-a - Installation

La première commande disponible est migrate:install qui est destinée à créer la table des migrations dans la base :

Cette opération n'est pas indispensable parce que si cette table n'existe pas elle sera créée lors de la première migration effectuée.

XIII-A-5-b - Première migration

Créons maintenant notre première migration. Imaginons que nous voulons une table d'auteurs. On commence par créer la migration en lui donnant un nom explicite, et tant qu'à faire on utilise les options pour générer le maximum de code :

Le fichier est créé :

Avec ce code :

1. use Illuminate\Database\Schema\Blueprint;

2. use Illuminate\Database\Migrations\Migration;

4. class CreateAuteursTable extends Migration {

6. /**

7. * Run the migrations.

8. *

9. * @return void

10. */

11. public function up()

12. {

13. Schema::create('auteurs', function(Blueprint $table)

14. {

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

16. $table->timestamps();

17. });

18. }

20. /**

21. * Reverse the migrations.

22. *

23. * @return void

24. */

25. public function down()

26. {

27. Schema::drop('auteurs');

28. }

30. }

Ajoutons un champ, par exemple le nom de l'auteur :

1. Schema::create('auteurs', function(Blueprint $table)

2. {

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

4. $table->timestamps();

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

6. });

Et maintenant activons cette migration :

La table « auteurs » est bien créée avec les champs attendus :

XIII-A-5-c - Ajout d'un champ

Maintenant on se rend compte qu'on a oublié le champ « email », comment l'ajouter ? On pourrait faire un rollback pour supprimer la table, faire la modification et relancer la migration. Mais il est plus simple de créer une nouvelle migration pour cet ajout :

Rappelez-vous que vous donnez le nom que vous voulez à la migration, faites en sorte que ce soit explicite pour vous. On a donc un nouveau fichier :

Avec ce code :

1. use Illuminate\Database\Schema\Blueprint;

2. use Illuminate\Database\Migrations\Migration;

4. class AddEmailToAuteurs extends Migration {

6. /**

7. * Run the migrations.

8. *

9. * @return void

10. */

11. public function up()

12. {

13. //

14. }

16. /**

17. * Reverse the migrations.

18. *

19. * @return void

20. */

21. public function down()

22. {

23. //

24. }

26. }

Ajoutons le code pour notre email :

1. public function up()

2. {

3. Schema::table('auteurs', function($table) {

4. $table->string('email', 64);

5. });

6. }

8. ....

10. public function down()

11. {

12. Schema::table('auteurs', function($table) {

13. $table->dropColumn('email');

14. });

15. }

Et lançons cette migration :

XIII-A-5-d - Retour en arrière

Finalement on se rend compte qu'on n'a pas besoin de l'email, alors on supprime la dernière migration :

On a bien la suppression de l'email dans la base.

XIII-A-5-e - Ajout d'une table

Maintenant on veut ajouter une table des livres :

Avec encore le code de base :

1. use Illuminate\Database\Schema\Blueprint;

2. use Illuminate\Database\Migrations\Migration;

4. class CreateLivresTable extends Migration {

6. /**

7. * Run the migrations.

8. *

9. * @return void

10. */

11. public function up()

12. {

13. Schema::create('livres', function(Blueprint $table)

14. {

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

16. $table->timestamps();

17. });

18. }

20. /**

21. * Reverse the migrations.

22. *

23. * @return void

24. */

25. public function down()

26. {

27. Schema::drop('livres');

28. }

30. }

Ajoutons un champ :

1. Schema::create('livres', function(Blueprint $table)

2. {

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

4. $table->timestamps();

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

6. });

Et lançons la migration :

Remarquez que deux migrations ont eu lieu, et c'est logique. Puisqu'on avait fait un rollback pour l'email, cette migration existe toujours et n'est pas effectuée, donc la commande migrate l'active. Si on avait voulu éviter cela il aurait suffit de supprimer le fichier correspondant qui n'avait plus de raison d'être si cet email était vraiment à supprimer. On trouve bien la table des livres avec les champs attendus :

XIII-A-5-f - Épurer une migration

Revenons sur le cas de l'email . On se rend compte que finalement on n'en voulait pas, comment le supprimer maintenant ? Faisons deux rollbacks :

Maintenant supprimons le fichier de la migration de l'email :

Et faisons une migration :

Nous obtenons bien ce que nous voulions : suppression de l'email et création de la table des livres.

XIII-A-5-g - Rafraichissement des migrations

Il est parfois utile de relancer toutes les migrations (par exemple si on a effectué quelques modifications dans les fichiers des migrations) Pour faire cela il y a la commande refresh :

On a les rollbacks dans l'ordre et les migrations qui se relancent, dans l'ordre aussi.

XIII-A-5-h - Suppression de toutes les migrations

Enfin on peut aussi vouloir supprimer toutes les migrations effectuées, on le fait avec la commande reset :

Cette fois on n'a que les rollbacks.

***

Nous avons donc vu comment gérer la structure d'une base de données, nous verrons dans une prochaine étape comment manipuler des données.

XIV - Chapitre 14 : Les bases de données 2/3

Nous avons vu précédemment comment modifier la structure d'une base. Nous allons voir maintenant comment gérer les enregistrements.

XIV-A - Des tables d'exemple

Pour expérimenter la gestion des enregistrements nous allons avoir besoin de tables. Nous allons utiliser ce que nous avons vu la dernière fois pour créer deux tables. Créons une migration pour une table auteurs et une table livres :

Le code pour la migration de la table des auteurs app/database/migrations/2013_01_24_195027_create_auteurs.php (évidemment vous aurez des références différentes):

1. class CreateAuteurs extends Migration {

2. public function up()

3. {

4. Schema::create('auteurs', function($table) {

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

6. $table->string('nom', 50);

7. $table->string('prenom', 50);

8. $table->date('naissance');

9. });

10. }

11. public function down()

12. {

13. Schema::drop('auteurs');

14. }

15. }

Le code pour la migration de la table des livres app/database/migrations/2013_01_24_195231_create_livres.php (évidemment vous aurez là aussi des références différentes):

1. class CreateLivres extends Migration {

2. public function up()

3. {

4. Schema::create('livres', function($table) {

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

6. $table->string('titre', 50);

7. $table->integer('id_auteur');

8. });

9. }

10. public function down()

11. {

12. Schema::drop('livres');

13. }

14. }

Et enfin nous faisons la migration :

XIV-A-1 - Insertion de données

Nos tables sont un peu vides, nous allons maintenant les remplir. Créons des auteurs. Pour insérer un seul enregistrement, la syntaxe est la suivante :

DB::table('auteurs')->insert(
array('nom' => 'Flaubert', 'prenom' => 'Gustave', 'naissance' => '1821-12-12')
);

Pour en insérer plusieurs, il faut faire un tableau global (là j'ai dû un peu fouiller dans le code parce que l'exemple de la documentation est incorrect, ça fait partie des plaisirs des frameworks en version bêta  ) :

1. DB::table('auteurs')->insert(

2. array(

3. array('nom' => 'Raspail', 'prenom' => 'Jean', 'naissance' => '1929-01-24'),

4. array('nom' => 'Avril', 'prenom' => 'Nicole', 'naissance' => '1939-08-15'),

5. array('nom' => 'Pinger', 'prenom' => 'Robert', 'naissance' => '1919-07-19'),

6. array('nom' => 'Sibran', 'prenom' => 'Anne', 'naissance' => '1963-02-04'),

7. array('nom' => 'Saulers', 'prenom' => 'Pilippe', 'naissance' => '1969-06-12'),

8. array('nom' => 'July', 'prenom' => 'Serge', 'naissance' => '1942-12-27')

9. )

10. );

Pour tester ce code vous pouvez le faire directement dans une route. Ce n'est pas très joli mais parfait pour un test. Normalement ça devrait fonctionner :

Notez que la méthode insert renvoie true si l'insertion s'est bien effectuée.

On va aussi entrer quelques livres :

1. DB::table('livres')->insert(

2. array(

3. array('titre' => 'Secouons le cocotier', 'id_auteur' => '2'),

4. array('titre' => 'Pêcheurs de Lune', 'id_auteur' => '2'),

5. array('titre' => 'Les Royaumes de Borée', 'id_auteur' => '2'),

6. array('titre' => 'Le Quartier évanoui', 'id_auteur' => '5'),

7. array('titre' => 'Hugo et les Lapins', 'id_auteur' => '5'),

8. array('titre' => 'Le Salon des artistes', 'id_auteur' => '7')

9. )

10. );

XIV-A-2 - Sélectionner des données

Maintenant que nous avons des données nous allons voir comment en sélectionner certaines. Commençons par quelque chose de simple en sélectionnant tous les enregistrements d'une table :

$auteurs = DB::table('auteurs')->get();
var_dump($auteurs);

On obtient un tableau avec toutes les données sous forme d'objets :

1. array (size=7)

2. 0 =>

3. object(stdClass)[118]

4. public 'id' => int 1

5. public 'nom' => string 'Flaubert' (length=8)

6. public 'prenom' => string 'Gustave' (length=7)

7. public 'naissance' => string '1821-12-12' (length=10)

8. 1 =>

9. object(stdClass)[119]

10. public 'id' => int 2

11. public 'nom' => string 'Raspail' (length=7)

12. public 'prenom' => string 'Jean' (length=4)

13. public 'naissance' => string '1929-01-24' (length=10)

14. 2 =>

15. object(stdClass)[120]

16. public 'id' => int 3

17. public 'nom' => string 'Avril' (length=5)

18. public 'prenom' => string 'Nicole' (length=6)

19. public 'naissance' => string '1939-08-15' (length=10)

20. 3 =>

21. object(stdClass)[121]

22. public 'id' => int 4

23. public 'nom' => string 'Pinger' (length=6)

24. public 'prenom' => string 'Robert' (length=6)

25. public 'naissance' => string '1919-07-19' (length=10)

26. 4 =>

27. object(stdClass)[122]

28. public 'id' => int 5

29. public 'nom' => string 'Sibran' (length=6)

30. public 'prenom' => string 'Anne' (length=4)

31. public 'naissance' => string '1963-02-04' (length=10)

32. 5 =>

33. object(stdClass)[123]

34. public 'id' => int 6

35. public 'nom' => string 'Saulers' (length=7)

36. public 'prenom' => string 'Pilippe' (length=7)

37. public 'naissance' => string '1969-06-12' (length=10)

38. 6 =>

39. object(stdClass)[124]

40. public 'id' => int 7

41. public 'nom' => string 'July' (length=4)

42. public 'prenom' => string 'Serge' (length=5)

43. public 'naissance' => string '1942-12-27' (length=10)

Si on veut affiner la sélection il faut utiliser une clause where :

$auteurs = DB::table('auteurs')->where('nom', 'Pinger')->get();
var_dump($auteurs);

1. array (size=1)

2. 0 =>

3. object(stdClass)[118]

4. public 'id' => int 4

5. public 'nom' => string 'Pinger' (length=6)

6. public 'prenom' => string 'Robert' (length=6)

7. public 'naissance' => string '1919-07-19' (length=10)

Si on sélectionne plusieurs enregistrements et qu'on désire seulement le premier :

$auteurs = DB::table('auteurs')->where('nom', '>', 'r')->first();
var_dump($auteurs);

1. object(stdClass)[118]

2. public 'id' => int 2

3. public 'nom' => string 'Raspail' (length=7)

4. public 'prenom' => string 'Jean' (length=4)

5. public 'naissance' => string '1929-01-24' (length=10)

Si on veut juste une colonne d'un enregistrement :

$auteurs = DB::table('auteurs')->where('id', 2)->pluck('nom');
var_dump($auteurs);

string 'Raspail' (length=7)

Si on veut seulement certaines colonnes :

$auteurs = DB::table('auteurs')->select('nom', 'prenom')->where('nom', '>', 'r')->get();
var_dump($auteurs);

1. array (size=3)

2. 0 =>

3. object(stdClass)[118]

4. public 'nom' => string 'Raspail' (length=7)

5. public 'prenom' => string 'Jean' (length=4)

6. 1 =>

7. object(stdClass)[119]

8. public 'nom' => string 'Sibran' (length=6)

9. public 'prenom' => string 'Anne' (length=4)

10. 2 =>

11. object(stdClass)[120]

12. public 'nom' => string 'Saulers' (length=7)

13. public 'prenom' => string 'Pilippe' (length=7)

Combiner des clauses where :

1. $auteurs = DB::table('auteurs')->where('naissance', '<' , '1900-01-01')

2. ->orWhere('naissance', '>' , '1960-01-01')

3. ->get();

4. var_dump($auteurs);

1. array (size=3)

2. 0 =>

3. object(stdClass)[118]

4. public 'id' => int 1

5. public 'nom' => string 'Flaubert' (length=8)

6. public 'prenom' => string 'Gustave' (length=7)

7. public 'naissance' => string '1821-12-12' (length=10)

8. 1 =>

9. object(stdClass)[119]

10. public 'id' => int 5

11. public 'nom' => string 'Sibran' (length=6)

12. public 'prenom' => string 'Anne' (length=4)

13. public 'naissance' => string '1963-02-04' (length=10)

14. 2 =>

15. object(stdClass)[120]

16. public 'id' => int 6

17. public 'nom' => string 'Saulers' (length=7)

18. public 'prenom' => string 'Pilippe' (length=7)

19. public 'naissance' => string '1969-06-12' (length=10)

Mettre une valeur dans un intervalle :

$auteurs = DB::table('auteurs')->whereBetween('naissance', array('1930-01-01', '1960-01-01'))->get();
var_dump($auteurs);

1. array (size=2)

2. 0 =>

3. object(stdClass)[118]

4. public 'id' => int 3

5. public 'nom' => string 'Avril' (length=5)

6. public 'prenom' => string 'Nicole' (length=6)

7. public 'naissance' => string '1939-08-15' (length=10)

8. 1 =>

9. object(stdClass)[119]

10. public 'id' => int 7

11. public 'nom' => string 'July' (length=4)

12. public 'prenom' => string 'Serge' (length=5)

13. public 'naissance' => string '1942-12-27' (length=10)

Ordonner le résultat :

$auteurs = DB::table('auteurs')->select('nom')->orderBy('nom', 'desc')->get();
var_dump($auteurs);

1. array (size=7)

2. 0 =>

3. object(stdClass)[118]

4. public 'nom' => string 'Sibran' (length=6)

5. 1 =>

6. object(stdClass)[119]

7. public 'nom' => string 'Saulers' (length=7)

8. 2 =>

9. object(stdClass)[120]

10. public 'nom' => string 'Raspail' (length=7)

11. 3 =>

12. object(stdClass)[121]

13. public 'nom' => string 'Pinger' (length=6)

14. 4 =>

15. object(stdClass)[122]

16. public 'nom' => string 'July' (length=4)

17. 5 =>

18. object(stdClass)[123]

19. public 'nom' => string 'Flaubert' (length=8)

20. 6 =>

21. object(stdClass)[124]

22. public 'nom' => string 'Avril' (length=5)

Utiliser un offset et une limite :

$auteurs = DB::table('auteurs')->select('nom')->skip(3)->take(2)->get();
var_dump($auteurs);

1. array (size=2)

2. 0 =>

3. object(stdClass)[118]

4. public 'nom' => string 'Pinger' (length=6)

5. 1 =>

6. object(stdClass)[119]

7. public 'nom' => string 'Sibran' (length=6)

Il existe encore bien d'autres possibilités et je vous renvoie à la documentation.

XIV-A-3 - Les jointures

Il arrive souvent qu'on doive utiliser des jointures lorsque deux (ou plusieurs) tables sont concernées. Voici un exemple :

1. $livres = DB::table('livres')

2. ->join('auteurs', 'auteurs.id', '=', 'livres.id_auteur')

3. ->where('nom', '=', 'Raspail')

4. ->select('titre')

5. ->get();

6. var_dump($livres);

1. array (size=3)

2. 0 =>

3. object(stdClass)[119]

4. public 'titre' => string 'Secouons le cocotier' (length=20)

5. 1 =>

6. object(stdClass)[120]

7. public 'titre' => string 'Pêcheurs de Lune' (length=17)

8. 2 =>

9. object(stdClass)[121]

10. public 'titre' => string 'Les Royaumes de Borée' (length=22)

XIV-A-4 - Mise à jour et suppression d'enregistrements

Voyons à présent une mise à jour :

1. $livres = DB::table('livres')

2. ->where('titre', '=', 'Pêcheurs de Lune')

3. ->update(array('id_auteur' => 3));

4. var_dump($livres);

int 1

On a en retour le nombre d'enregistrements affectés. Dans notre cas on a un seul enregistrement.

Voyons une suppression :

$livres = DB::table('livres')->where('id_auteur', '=', 5)->delete();
var_dump($livres);

int 2

On a aussi en retour le nombre d'enregistrements affectés. Ici deux puisque nous avons supprimé les deux ouvrages d'Anne Sibran :

Je vous renvoie à la documentation pour toutes les informations complémentaires. Mais surtout, n'oubliez pas qu'elle est encore en évolution .

XV - Chapitre 15 : Les bases de données 3/3

Maintenant que nous savons mettre en place la structure d'une base de données et intervenir sur les enregistrements que nous reste-t-il à voir ? Et bien Laravel nous propose un ORM vraiment efficace nommé Eloquent. Qu'est-ce qu' un ORM ? C'est un « Object Relational Mapper ». Mapper parce qu'on va « mapper » des objets PHP sur les tables et les colonnes d'une base. Relational parce qu'on va aussi pouvoir gérer les relations de notre base. Mais tout cela va devenir plus clair avec des exemples. Vous n'avez pas besoin d'un ORM pour gérer une base, mais vous le ferez avec beaucoup plus de facilité avec lui.

Je pars du principe que vous avez une base nommée laravel telle que décrite lors du chapitre précédent avec deux tables : auteurs et livres. Vous avez aussi correctement configuré le fichier app/config/database.php pour accéder à la base.

XV-A - Créer un modèle avec Eloquent

Voyons comment créer un modèle avec Eloquent. Par exemple pour notre table auteurs. Créez un fichier avec ce code et enregistrez-le dans app/models/Auteur.php :

<?php
class Auteur extends Eloquent {}

Et voilà vous avez créé un modèle et vous pouvez maintenant l'utiliser . Bon il a quand même fallu prendre quelques précautions : j'ai nommé la classe Auteur, autrement dit le nom de la table avec le « s » en moins. En effet la table peut contenir plusieurs auteurs mais la classe s'adresse à un auteur à chaque fois. D'autre part il est impératif que la table contienne une clé nommée « primaire id », ce qui est notre cas.

XV-A-1 - Extraire des données

Il ne nous reste plus qu'à utiliser notre nouvelle classe. Dans une route entrez ce code :

$auteur = Auteur::find(1);
var_dump($auteur);

On demande l'enregistrement avec l'index 1. Voyons ce que nous avons en retour :

1. object(Auteur)[118]

2. protected 'connection' => null

3. protected 'table' => null

4. protected 'primaryKey' => string 'id' (length=2)

5. protected 'perPage' => int 15

6. public 'incrementing' => boolean true

7. public 'timestamps' => boolean true

8. protected 'attributes' =>

9. array (size=4)

10. 'id' => int 1

11. 'nom' => string 'Flaubert' (length=8)

12. 'prenom' => string 'Gustave' (length=7)

13. 'naissance' => string '1821-12-12' (length=10)

14. protected 'original' =>

15. array (size=4)

16. 'id' => int 1

17. 'nom' => string 'Flaubert' (length=8)

18. 'prenom' => string 'Gustave' (length=7)

19. 'naissance' => string '1821-12-12' (length=10)

20. protected 'relations' =>

21. array (size=0)

22. empty

23. protected 'hidden' =>

24. array (size=0)

25. empty

26. protected 'fillable' =>

27. array (size=0)

28. empty

29. protected 'guarded' =>

30. array (size=0)

31. empty

32. protected 'dates' =>

33. array (size=0)

34. empty

35. public 'exists' => boolean true

Maintenant nous comprenons mieux la notion de mappage : Nous obtenons un objet que nous pouvons exploiter pour extraire des informations. Ce qui est bien pratique aussi, c'est que tout ce que nous avons vu au chapitre précédent concernant la gestion des enregistrements reste valable. Ainsi nous pouvons écrire :

$auteur = Auteur::where('nom', '=', 'Saulers')->get();
var_dump($auteur[0]->nom);

Ce qui donne :

string 'Saulers' (length=7)

Eloquent nous renvoie une collection qui implémente l'interface IteratorAggregate. Nous pouvons donc agir comme si nous avions un tableau en retour. En effet notre requête est susceptible de renvoyer plusieurs lignes, contrairement au cas précédent où on appelait juste une ligne en précisant son index. Il devient alors facile de traiter les lignes lorsqu'elles nous arrivent sous cette forme :

$auteurs = Auteur::where('nom', '>', 'q')->get();
foreach ($auteurs as $auteur) var_dump($auteur->nom);

Ce qui donne :

string 'Raspail' (length=7)
string 'Sibran' (length=6)
string 'Saulers' (length=7)

XV-A-2 - Insérer des données

Nous allons voir qu'il est aussi très facile d'insérer de nouvelles données. Mais auparavant je dois vous donner encore une petite indication. Par défaut Eloquent gère deux colonnes dans chaque table de type DateTime nommées respectivement updated_at et created_at. Ces colonnes sont automatiquement mises à jour à chaque création ou modification. Mais vous n'êtes pas obligé de les avoir. Si ces informations ne vous intéressent pas il suffit de compléter le modèle ainsi :

1. <?php

2. class Auteur extends Eloquent

3. {

4. public $timestamps = false;

5. }

C'est ce que nous allons faire dans notre cas parce que nous n'avons pas prévu ces colonnes dans nos tables. Sachez juste que cette possibilité existe et nous l'utiliserons dans un article ultérieur sur la création d'un blog. Maintenant voilà le code pour faire une insertion :

1. $auteur = new Auteur;

2. $auteur->nom = 'Frain';

3. $auteur->prenom = 'Irène';

4. $auteur->naissance = '1950-06-22';

5. $auteur->save();

On vérifie dans la base :

Apparemment tout se passe bien. Vous pouvez aussi passer un tableau au constructeur :

1. $auteur = new Auteur(array(

2. 'nom' => 'Frain',

3. 'prenom' => 'Irène',

4. 'naissance' => '1950-06-22'

5. ));

6. $auteur->save();

XV-A-3 - Modifier et supprimer des données

Pour modifier une donnée existante la syntaxe est tout aussi simple, voici un exemple :

$auteur = Auteur::find(8);
$auteur->prenom = 'Claire';
$auteur->save();

Pour supprimer des données c'est tout aussi simple :

$auteur = Auteur::find(8);
$auteur->delete();

XV-A-4 - Les relations

Par définition dans une base de données relationnelle nous avons des… relations. Celles-ci peuvent être explicites dans la base ou implicites (comme c'est le cas dans notre exemple où on a juste introduit une ligne dans la table des livres pour préciser l'id de l'auteur mais sans demander à MySQL de vérifier l'intégrité de cette relation. On aurait pu le faire en précisant que nous utilisons le moteur InnoDB et en déclarant cette ligne comme clé étrangère).

Eloquent a une façon très élégante de gérer les relations. Nous allons prendre comme exemple celle qui existe entre nos deux tables. Mais auparavant il nous faut créer aussi un modèle pour la table des livres (app/models/Livre.php) :

1. class Livre extends Eloquent

2. {

3. public $timestamps = false;

4. }

Il existe trois sortes de relations : « un vers un », « un vers plusieurs » et « plusieurs vers plusieurs ». Notre cas est le plus classique avec une relation « un vers plusieurs », en effet un auteur peut écrire plusieurs livres et on admet qu'un livre n'est écrit que par un auteur (bon on oublie les coauteurs ).

Pour qu'Eloquent reconnaisse automatiquement la clé étrangère dans une table il faut qu'elle réponde à une certaine syntaxe : le nom de la table suivi d'un soulignement puis de « id ». Manque de chance j'ai fait juste l'inverse dans la table des livres. On va corriger ça :

Cette norme n'est pas une contrainte absolue parce que d'une part on peut préciser le nom de la clé étrangère comme paramètre des méthodes relationnelles, d'autre part elle n'est pas toujours respectée par Eloquent (par exemple pour belongsTo c'est le nom de la méthode qui sert de référence). Pour avoir une vue d'ensemble du sujet, référezvous au chapitre 33.

Maintenant nous pouvons utiliser toute la puissance et l'élégance d'Eloquent. Nous modifions la classe Auteur pour déclarer la relation :

1. class Auteur extends Eloquent

2. {

3. public $timestamps = false;

4. public function livres()

5. {

6. return $this->hasMany('Livre');

7. }

8. }

Et maintenant ça devient facile :

$livres = Auteur::find(2)->livres;
foreach($livres as $livre) var_dump($livre->titre);

Résultat :

string 'Secouons le cocotier' (length=20)
string 'Les Royaumes de Borée' (length=22)

On peut mettre en œuvre toutes les possibilités de manipulation de données. Par exemple si on ne veut que le premier livre :

$livre = Auteur::find(2)->livres()->first();
var_dump($livre->titre);

Résultat :

string 'Secouons le cocotier' (length=20)

XV-A-5 - Inverse d'une relation

Considérons à nouveau notre relation en la regardant dans l'autre sens. Nous avons dit qu'un auteur peut écrire plusieurs livres. Nous avons traduit cela dans la classe Auteur par ce code :

1. public function livres()

2. {

3. return $this->hasMany('Livre');

4. }

On pourrait aussi dire qu'un livre appartient à un auteur. Cette fois au lieu de renseigner la relation dans la classe Auteur on le fait dans la classe Livre :

1. <?php

2. class Livre extends Eloquent

3. {

4. public $timestamps = false;

5. public function auteur()

6. {

7. return $this->belongsTo('Auteur');

8. }

9. }

Testons cela :

$auteur = Livre::find(1)->auteur;
var_dump($auteur->nom);

Résultat :

string 'Raspail' (length=7)

XV-A-6 - Chargement dynamique

Avec la relation précédente, considérez ce code :

foreach(Livre::all() as $livre)
echo '"'.$livre->titre.'" a été écrit par '.$livre->auteur->nom.'<br>';

Et son résultat :

1. "Secouons le cocotier" a été écrit par Raspail

2. "Pêcheurs de Lune" a été écrit par Avril

3. "Les Royaumes de Borée" a été écrit par Raspail

4. "Le Salon des artistes" a été écrit par July

Le résultat correspond à notre attente mais… nous ne sommes pas très efficaces parce que nous multiplions les requêtes. Il y en a une pour récupérer tous les livres, et ensuite une pour chaque livre afin de trouver l'auteur. Imaginez que nous ayons des milliers de livres ! Heureusement il existe une solution :

foreach(Livre::with('auteur')->get() as $livre)
echo '"'.$livre->titre.'" a été écrit par '.$livre->auteur->nom.'<br>';

Cette fois on a une seule requête dans la boucle, qui prend la forme :

SELECT * FROM auteurs WHERE id IN (2,3,7);

Il y aurait encore beaucoup à dire sur Eloquent. Je vous renvoie encore une fois à la documentation. Vous pouvez également consulter le chapitre 33 qui est consacré à ce sujet.

XVI - Chapitre 16 : Authentification

Il est fréquent d'avoir besoin de filtrer les utilisateurs d'un site selon les pages affichées, les fonctions disponibles… On parle alors d'authentification. Laravel nous offre aussi une infrastructure efficace et facile à mettre en œuvre pour cette partie importante et délicate d'un site.

XVI-A - La table des utilisateurs

La première chose à faire est de créer une table dans notre base de données pour mémoriser les informations des utilisateurs. En général on se contente d'un pseudonyme, une adresse mail et un mot de passe. Par défaut la table doit se nommer users. On peut utiliser un autre nom, mais ça nous obligerait à effectuer quelques modifications, alors autant prendre la valeur par défaut. Commençons par faire une migration pour cette table comme nous l'avons vu lors du chapitre 133 :

Et renseignons notre code pour créer la table et aussi prévoir la marche arrière au cas où (je rappelle que le fichier a été créé dans le dossier database/migrations) :

1. use Illuminate\Database\Schema\Blueprint;

2. use Illuminate\Database\Migrations\Migration;

4. class CreateUsers extends Migration {

6. /**

7. * Run the migrations.

8. *

9. * @return void

10. */

11. public function up()

12. {

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

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

15. $table->string('username', 128)->unique();

16. $table->string('password', 64);

17. $table->string('remember_token', 100)->nullable();

18. });

19. }

21. /**

22. * Reverse the migrations.

23. *

24. * @return void

25. */

26. public function down()

27. {

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

29. }

31. }

Vous pouvez changer le nom pour la ligne du pseudo, mais ne changez pas celui du mot de passe qui doit être password, ou alors relevez les manches et allez modifier quelques lignes de code . Remarquez aussi que j'ai précisé que le nom doit être unique, cela va créer automatiquement un index pour cette ligne. Le champ remember_token a été introduit avec la version 4.1.26 de Laravel pour des raisons de sécurité. Il ne reste plus qu'à effectuer la migration :

On vérifie que la table est bien créée :

C'est vraiment la prestation minimale mais ça suffira pour notre exemple. Si vous regardez maintenant dans le dossier app/models vous trouvez un modèle déjà existant nommé User. Si vous l'ouvrez, vous trouvez ce code :

1. use Illuminate\Auth\UserInterface;

2. use Illuminate\Auth\Reminders\RemindableInterface;

4. class User extends Eloquent implements UserInterface, RemindableInterface {

6. /**

7. * The database table used by the model.

8. *

9. * @var string

10. */

11. protected $table = 'users';

13. /**

14. * The attributes excluded from the model's JSON form.

15. *

16. * @var array

17. */

18. protected $hidden = array('password');

20. /**

21. * Get the unique identifier for the user.

22. *

23. * @return mixed

24. */

25. public function getAuthIdentifier()

26. {

27. return $this->getKey();

28. }

30. /**

31. * Get the password for the user.

32. *

33. * @return string

34. */

35. public function getAuthPassword()

36. {

37. return $this->password;

38. }

40. /**

41. * Get the token value for the "remember me" session.

42. *

43. * @return string

44. */

45. public function getRememberToken()

46. {

47. return $this->remember_token;

48. }

50. /**

51. * Set the token value for the "remember me" session.

52. *

53. * @param  string  $value

54. * @return void

55. */

56. public function setRememberToken($value)

57. {

58. $this->remember_token = $value;

59. }

61. /**

62. * Get the column name for the "remember me" token.

63. *

64. * @return string

65. */

66. public function getRememberTokenName()

67. {

68. return 'remember_token';

69. }

71. /**

72. * Get the e-mail address where password reminders are sent.

73. *

74. * @return string

75. */

76. public function getReminderEmail()

77. {

78. return $this->email;

79. }

81. }

Laravel est vraiment gentil avec nous !

XVI-A-1 - Insertion d'utilisateurs

On va maintenant créer quelques utilisateurs mais il nous faut un peu parler du mot de passe. Celui-ci doit être chiffré pour des raisons de sécurité. Laravel nous propose la classe Hash pour effectuer ce chiffrement. Voilà le code pour ajouter deux utilisateurs :

1. DB::table('users')->insert(

2. array(

3. array('username' => 'LeGros', 'password' => Hash::make('passeLeGros')),

4. array('username' => 'LeMaigre', 'password' => Hash::make('passeLeMaigre'))

5. )

6. );

Vérifions dans la base :

Le résultat est satisfaisant et les mots de passe sont correctement chiffrés.

XVI-A-2 - Un formulaire de connexion

Créons à présent un formulaire de connexion app/views/connexion.php :

1. <!doctype html>

2. <html>

3. <head>

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

5. </head>

6. <body>

7. <h2>Connexion au site : </h2>

8. <?php

9. echo Form::open(array('url' => 'connexion'));

10. echo '<strong>',Form::label('nom', 'Nom :'),'</strong>';

11. echo Form::text('nom');

12. echo '<strong>',Form::label('password', 'Mot de passe :'),'</strong>';

13. echo Form::password('password');

14. echo Form::submit('Se connecter');

15. echo Form::close();

16. ?>

17. </body>

18. </html>

Et une route :

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

2. {

3. return View::make('connexion');

4. });

Avec l'URL http://localhost/laravel/public on obtient le formulaire :

XVI-A-3 - Traitement de la réponse au formulaire

Entrez ce code dans les routes :

1. Route::post('connexion', function()

2. {

3. $nom = Input::get('nom');

4. $passe = Input::get('password');

6. if(Auth::attempt(array('username' => $nom, 'password' => $passe)))

7. echo 'Vous êtes maintenant connecté '.Auth::user()->username;

8. else

9. echo 'Echec de la connexion';

10. });

On utilise la méthode attempt en passant comme paramètres le mot de passe et le nom. À la place du nom ça pourrait être autre chose comme un email, l'essentiel étant que cette information soit unique pour identifier l'utilisateur, ce qu'on a prévu dans la table pour les noms. Vous pouvez ajouter d'autres lignes, par exemple pour tester que l'utilisateur est actif, qu'il n'a pas été banni… Dans notre cas si le nom et le mot de passe correspondent, alors l'utilisateur est connecté, sinon il ne l'est pas. Laravel prend tout en charge, vous n'avez rien de plus à faire .

XVI-A-4 - Filtrage des utilisateurs

L'intérêt d'une authentification est évidemment de filtrer les utilisateurs pour ne leur donner accès qu'à des ressources autorisées en fonction de leur statut. Voici une route qui applique un filtrage :

1. Route::get('zone_reservee', array('before' => 'auth', function()

2. {

3. echo 'Vous avez bien été identifié '.Auth::user()->username;

4. }));

La syntaxe est limpide ! Mais où se trouve ce filtre ? Nous avons vu que tous les filtres se trouvent dans le fichier app/filters, c'est donc ici que nous trouvons les filtres de l'authentification :

1. Route::filter('auth', function()

2. {

3. if (Auth::guest()) return Redirect::route('login');

4. });

6. Route::filter('guest', function()

7. {

8. if (Auth::check()) return Redirect::to('/');

9. });

On en trouve deux : le premier qu'on a utilisé ci-dessus auth qui vérifie si l'utilisateur est bien authentifié. Qu'est-ce que cet Auth::guest() qu'on trouve à l'intérieur ? Juste une méthode qui renvoie true si l'utilisateur n'est pas authentifié, et vous voyez que dans ce cas on le renvoie à la route login pour qu'il le fasse.

Le second filtre guest fait exactement l'inverse. Il teste que l'utilisateur est authentifié avec Auth::check(), et dans ce cas on le renvoie sur le site auquel il a accès.

Maintenant que nous avons analysé ces filtres, nous pouvons écrire des routes qui les utilisent, histoire d'avoir quelque chose de fonctionnel :

1. Route::get('/', array('before' => 'auth', function()

2. {

3. echo 'Maintenant vous êtes sur le site '.Auth::user()->username;

4. }));

6. Route::get('login', array('as' => 'login', 'before' => 'guest', function()

7. {

8. return View::make('connexion');

9. }));

11. Route::post('connexion', function()

12. {

13. $nom = Input::get('nom');

14. $passe = Input::get('password');

16. if(Auth::attempt(array('username' => $nom, 'password' => $passe)))

17. return Redirect::to('/');

18. else

19. return Redirect::route('login');

20. });

Je vous laisse analyser ce code, conjointement avec celui des deux filtres pour comprendre comment tout ça se boucle. Remarquez que j'ai nommé la route login parce que le filtre attend ce nom de route ; j'aurais aussi pu changer la syntaxe du filtre mais comme ça vous voyez à nouveau un nommage de route .

XVI-A-5 - Se rappeler de moi

On prévoit souvent sur les sites une option « Se rappeler de moi », pour éviter d'avoir à s'authentifier à nouveau la prochaine fois qu'on visite le site. Ici aussi Laravel nous simplifie la vie. Mais voyons déjà les cookies utilisés pour une authentification simple :

On trouve deux cookies, le premier valide pour la durée de la session et le second valide pendant deux heures. Modifions le code pour avoir l'option « Se rappeler de moi » :

if(Auth::attempt(array('username' => $nom, 'password' => $passe), true))

J'ai juste ajouté un paramètre à la méthode attempt avec la valeur true. Regardons maintenant les cookies quand je me connecte :

On voit apparaître un troisième cookie nommé remember qui expire en 2018 .

XVI-A-6 - Protection CSRF

Il est toujours prudent de se protéger contre le cross-site et Laravel implante systématiquement cette protection pour les formulaires. Regardez le contrôle caché qui a été généré dans le nôtre :

<input type="hidden" value="7TorWnP8iZUxyeet6z8MfKAZnt8Mz6qKZaSh64pU" name="_token">

Et la route pour le traitement du retour du formulaire est :

1. Route::post('connexion', array('before' => 'csrf', function()

2. {

3. $nom = Input::get('nom');

4. $passe = Input::get('password');

6. if(Auth::attempt(array('username' => $nom, 'password' => $passe)))

7. return Redirect::to('/');

8. else

9. return Redirect::route('login');

10. }));

Le jeton (token) est vérifié au niveau du filtre CSRF :

1. Route::filter('csrf', function()

2. {

3. if (Session::token() != Input::get('_token'))

4. {

5. throw new Illuminate\Session\TokenMismatchException;

6. }

7. });

On vérifie que le jeton envoyé correspond à celui qui a été placé en session.

On a fait le tour de l'essentiel de l'authentification, vous trouverez des informations complémentaires, comme d'habitude, dans la documentation.

XVII - Chapitre 17 : Validation

Laravel peut nous simplifier aussi la vie dans le domaine de la validation des entrées et la génération des messages d'erreur afférents.

XVII-A - Le formulaire d'inscription

Nous allons considérer un formulaire d'inscription app/views/inscription.php :

1. <h2>Inscription au site : </h2>

2. <form action="http://localhost/laravel/public/inscription" method="post">

3. <label for="nom"><strong>Nom :</strong></label>

4. <input type="text" name="nom" id="nom"/>

5. <label for="pass"><strong>Mot de passe :</strong></label>

6. <input type="text" name="passe" id="pass"/>

7. <label for="confirmepasse"><strong>Confirmation du mot de passe :</strong></label>

8. <input type="text" name="confirmepasse" id="confirmepasse"/>

9. <input type="submit" value="S'inscrire"/>

10. </form>

XVII-A-1 - Les routes

Il nous faut déjà une route pour afficher le formulaire avec l'URL http://localhost/laravel/public/login :

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

2. {

3. return View::make('inscription');

4. });

Il nous faut ensuite une route pour le traitement de retour du formulaire avec l'URL localhost/laravel/public/inscription, qui aura cette structure :

1. Route::post('inscription', function()

2. {

4. });

XVII-A-2 - La validation

La validation avec Laravel se fait en définissant des règles :

1. $rules = array(

2. 'nom' => 'required|min:5|max:20|alpha',

3. 'passe' => 'required|min:6|max:10|alpha',

4. 'confirmepasse' => 'required|same:passe'

5. );

On définit dans un tableau pour chaque champ les règles correspondantes. Vous pouvez trouver toutes les règles disponibles dans la documentation. Ici par exemple on veut que le nom soit saisi avec un minimum de cinq et un maximum de 20 caractères alphanumériques. Pour le champ confirmepasse on veut qu'il soit identique à passe. Ensuite on crée une instance de la classe Validation en spécifiant les valeurs des champs et les règles :

$validator = Validator::make(Input::all(), $rules);

Il ne reste plus qu'à tester que les règles ont été respectées avec la méthode fails :

if ($validator->fails()) {
// Traitement en cas d'échec dans au moins une des règles
}

On peut utiliser la méthode inverse passes :

if ($validator->passes()) {
// Traitement en cas de réussite
}

XVII-A-3 - Les messages

C'est bien de déterminer qu'une règle n'a pas été respectée, encore faut-il savoir laquelle. La classe Validation possède des méthodes pour nous donner des messages relatifs à ces non-respects. Voici notre route maintenant :

1. Route::post('inscription', function()

2. {

3. $rules = array(

4. 'nom' => 'required|min:5|max:20|alpha',

5. 'passe' => 'required|min:6|max:10|alpha',

6. 'confirmepasse' => 'required|same:passe'

7. );

8. $validator = Validator::make(Input::all(), $rules);

10. if ($validator->fails()) {

11. echo $validator->messages();

12. }

13. else echo 'Vous êtes incrit '.Input::get('nom');

14. });

Et quelques messages en cas de non-respect. Par exemple si on ne saisit rien du tout :

{"nom":["The nom field is required."],"passe":["The passe field is required."],"confirmepasse":["The confirmepasse field is required."]}

Avec un nom et un mot de passe trop courts et pas de confirmation du mot de passe :

{"nom":["The nom must be at least 5 characters."],"passe":["The passe must be at least 6 characters."],"confirmepasse":["The confirmepasse field is required."]}

Si la confirmation du mot de passe n'est pas correcte :

{"confirmepasse":["The confirmepasse and passe must match."]}

Bon tout ça semble bien fonctionner, mais c'est en anglais ! Regardez le fichier app/lang/en/validation.php :

1. <?php

3. return array(

5. /*

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

7. | Validation Language Lines

8. |--------------------------------------------------------------------------

9. |

10. | The following language lines contain the default error messages used by

11. | the validator class. Some of these rules have multiple versions such

12. | such as the size rules. Feel free to tweak each of these messages.

13. |

14. */

16. "accepted"        => "The :attribute must be accepted.",

17. "active_url"      => "The :attribute is not a valid URL.",

18. "after"           => "The :attribute must be a date after :date.",

19. "alpha"           => "The :attribute may only contain letters.",

20. "alpha_dash"      => "The :attribute may only contain letters, numbers, and dashes.",

21. "alpha_num"       => "The :attribute may only contain letters and numbers.",

22. "before"          => "The :attribute must be a date before :date.",

23. "between"         => array(

24. "numeric"         => "The :attribute must be between :min - :max.",

25. "file"            => "The :attribute must be between :min - :max kilobytes.",

26. "string"          => "The :attribute must be between :min - :max characters.",

27. ),

28. "confirmed"       => "The :attribute confirmation does not match.",

29. "different"       => "The :attribute and :other must be different.",

30. "digits"          => "The :attribute must be :digits digits.",

31. "digits_between"  => "The :attribute must be between :min and :max digits.",

32. "email"           => "The :attribute format is invalid.",

33. "exists"          => "The selected :attribute is invalid.",

34. "image"           => "The :attribute must be an image.",

35. "in"              => "The selected :attribute is invalid.",

36. "integer"         => "The :attribute must be an integer.",

37. "ip"              => "The :attribute must be a valid IP address.",

38. "match"           => "The :attribute format is invalid.",

39. "max"             => array(

40. "numeric"     => "The :attribute must be less than :max.",

41. "file"        => "The :attribute must be less than :max kilobytes.",

42. "string"      => "The :attribute must be less than :max characters.",

43. ),

44. "mimes"           => "The :attribute must be a file of type: :values.",

45. "min"             => array(

46. "numeric"     => "The :attribute must be at least :min.",

47. "file"        => "The :attribute must be at least :min kilobytes.",

48. "string"      => "The :attribute must be at least :min characters.",

49. ),

50. "notin"           => "The selected :attribute is invalid.",

51. "numeric"         => "The :attribute must be a number.",

52. "required"        => "The :attribute field is required.",

53. "required_with"   => "The :attribute field is required when :values is present.",

54. "same"            => "The :attribute and :other must match.",

55. "size"            => array(

56. "numeric"         => "The :attribute must be :size.",

57. "file"            => "The :attribute must be :size kilobytes.",

58. "string"          => "The :attribute must be :size characters.",

59. ),

60. "unique"          => "The :attribute has already been taken.",

61. "url"             => "The :attribute format is invalid.",

63. /*

64. |--------------------------------------------------------------------------

65. | Custom Validation Language Lines

66. |--------------------------------------------------------------------------

67. |

68. | Here you may specify custom validation messages for attributes using the

69. | convention "attribute.rule" to name the lines. This makes it quick to

70. | specify a specific custom language line for a given attribute rule.

71. |

72. */

74. 'custom' => array(),

76. /*

77. |--------------------------------------------------------------------------

78. | Custom Validation Attributes

79. |--------------------------------------------------------------------------

80. |

81. | The following language lines are used to swap attribute place-holders

82. | with something more reader friendly such as E-Mail Address instead

83. | of "email". This simply helps us make messages a little cleaner.

84. |

85. */

87. 'attributes' => array(),

89. );

Tous les messages sont ici et rien ne vous empêche de les traduire . Vous pouvez aussi récupérer le message créé par Caouecs ici. Mais si vous le faites, adoptez une méthode « propre » et créez un dossier fr pour localiser les fichiers de langues et renseignez la ligne correspondante dans le fichier app/config/app.php.

Il est aussi possible de passer le texte des messages comme troisième paramètre de cette façon :

1. $validator = Validator::make(

2. Input::all(),

3. $rules,

4. array('required' => 'Le champ :attribute est requis.',

5. ));

XVII-A-4 - Utilisation des messages

Maintenant que nous avons des messages, nous devons les afficher pour que l'utilisateur corrige sa saisie. Il faut donc transmettre au formulaire d'inscription ces erreurs et prévoir leur affichage. Pour la transmission c'est simple :

1. Route::post('inscription', function()

2. {

3. $rules = array(

4. 'nom' => 'required|min:5|max:20|alpha',

5. 'passe' => 'required|min:6|max:10|alpha',

6. 'confirmepasse' => 'required|same:passe'

7. );

8. $validator = Validator::make(Input::all(), $rules);

9. if ($validator->fails()) {

10. return Redirect::to('login')->withErrors($validator);

11. }

12. else echo 'Vous êtes incrit '.Input::get('nom');

13. });

Et pour le formulaire c'est tout aussi simple :

1. <form action="http://localhost/laravel/public/inscription" method="post">

2. <?php echo $errors->first('nom', '<p>:message</p>'); ?>

3. <label for="nom"><strong>Nom :</strong></label>

4. <input type="text" name="nom" id="nom"/>

5. <?php echo $errors->first('passe', '<p>:message</p>'); ?>

6. <label for="pass"><strong>Mot de passe :</strong></label>

7. <input type="text" name="passe" id="pass"/>

8. <?php echo $errors->first('confirmepasse', '<p>:message</p>'); ?>

9. <label for="confirmepasse"><strong>Confirmation du mot de passe :</strong></label>

10. <input type="text" name="confirmepasse" id="confirmepasse"/>

11. <input type="submit" value="S'inscrire"/>

12. </form>

Ce qui donne par exemple :

La méthode first récupère la première erreur rencontrée pour un champ donné. On peut aussi obtenir tous les messages pour un champ avec la méthode get :

$messages->get('nom')

Ou alors tester s'il y a des messages pour un champ :

$messages->has('nom')

XVII-A-5 - Renseigner les champs au retour d'erreurs de saisie

Bon il nous reste quand même un petit souci. Il serait bien de renseigner certains champs quand on réaffiche le formulaire suite à un non-respect des règles de saisie. La transmission des valeurs des champs se fait facilement :

1. Route::post('inscription', function()

2. {

3. $rules = array(

4. 'nom' => 'required|min:5|max:20|alpha',

5. 'passe' => 'required|min:6|max:10|alpha',

6. 'confirmepasse' => 'required|same:passe'

7. );

8. $validator = Validator::make(Input::all(), $rules);

9. if ($validator->fails()) {

10. return Redirect::to('login')->withErrors($validator)->withInput();

11. }

12. else echo 'Vous êtes incrit '.Input::get('nom');

13. });

Et voici comment les récupérer dans le formulaire avec la méthode old :

1. <form action="http://localhost/laravel/public/inscription" method="post">

2. <?php echo $errors->first('nom', '<p>:message</p>'); ?>

3. <label for="nom"><strong>Nom :</strong></label>

4. <input type="text" name="nom" id="nom" value="<?php echo Input::old('nom'); ?>" />

5. <?php echo $errors->first('passe', '<p>:message</p>'); ?>

6. <label for="pass"><strong>Mot de passe :</strong></label>

7. <input type="password" name="passe" id="pass" value="<?php echo Input::old('passe'); ?>" />

8. <?php echo $errors->first('confirmepasse', '<p>:message</p>'); ?>

9. <label for="confirmepasse"><strong>Confirmation du mot de passe :</strong></label>

10. <input type="password" name="confirmepasse" id="confirmepasse"/>

11. <input type="submit" value="S'inscrire"/>

12. </form>

Si vous utilisez la classe Form, le remplissage des champs avec les anciennes valeurs est automatique. Voici l'équivalent du formulaire ci-dessus, mais cette fois en utilisant les classes Form et Blade :

1. {{ $errors->first('nom', '<p>:message</p>') }}

2. {{ Form::open(array('url' => 'inscription')) }}

3. <strong>{{ Form::label('nom', 'Nom : ') }}</strong>

4. {{ Form::text('nom'); }}<br>

5. {{ $errors->first('passe', '<p>:message</p>') }}

6. <strong>{{ Form::label('pass', 'Mot de passe : ') }}</strong>

7. {{ Form::password('pass'); }}<br>

8. {{ $errors->first('confirmepasse', '<p>:message</p>') }}

9. <strong>{{ Form::label('confirmepasse', 'Confirmation du mot de passe :') }}</strong>

10. {{ Form::password('confirmepasse') }}<br>

11. {{ Form::submit('S\'inscrire') }}

12. {{ Form::close() }}

Vous n'êtes bien sûr pas obligés d'utiliser ces possibilités mais ce serait dommage .

XVIII - Chapitre 18 : Un blog : les données

Il est temps d'utiliser tout ce que nous avons vu dans une application pratique. Il est habituel de présenter un blog dans ce genre de situation, d'une part parce que c'est une application qui permet de brasser pas mal de fonctionnalités, et d'autre part parce que c'est quelque chose de très utile.

Article mis à jour pour la version 4.1.26 de Laravel.

XVIII-A - Installer et configurer Laravel

Nous n'allons pas faire quelque chose de très complexe mais de suffisamment structuré pour être significatif. La première chose à faire est d'installer et de configurer Laravel. Nous avons déjà vu comment on fait cela dans les chapitres 2 et 3. Je pars donc du principe que vous avez une installation toute neuve de Laravel dans un dossier nommé blog auquel on accède avec cet URL : http://localhost/blog/public.

XVIII-A-1 - La base de données

La structuration des données constitue la colonne vertébrale d'une application. Nous allons donc nous attarder sur ce point. Nous avons besoin pour notre blog de quatre tables : une pour les utilisateurs, une pour les catégories d'articles, une pour les articles et enfin une dernière pour les commentaires. Le but est de bâtir cette configuration :

Voyons de plus près chacune des tables :

- users : nous avons déjà eu affaire à cette table dans le chapitre sur l'authentification. J'ai juste prévu en plus un champ pour l'email et un autre pour le statut de l'utilisateur, qui est soit administrateur (qui peut créer des articles et gérer le site), soit simple intervenant (qui peut commenter des articles). Les champs created_at et updated_at sont gérés automatiquement par Laravel, on les retrouve dans toutes les tables. Le pseudo et l'email doivent être uniques.

- categories : ici j'ai prévu un titre et une description.

- articles : c'est la table forcément la plus chargée, on trouve un titre, un texte d'introduction intro_text (qui apparaîtra sur la page de la catégorie), un texte de contenu de l'article full_text (qui apparaîtra quand on sélectionnera l'article). J'ai aussi prévu un champ allow_comment pour signaler si on peut ou non commenter l'article. On trouve aussi deux clés étrangères, une qui référence l'utilisateur qui a écrit l'article user_id et une autre qui référence la catégorie categorie_id. Je pars du principe qu'on utilise MySQL avec InnoDB.

- comments : cette table accueillera les commentaires. Là j'ai prévu juste un titre et un champ pour le texte. On trouve aussi deux clés étrangères : une qui référence l'utilisateur qui a écrit le commentaire user_id et une autre qui référence l'article concerné article_id.

- password_reminders : cette table sert pour la réinitialisation du mot de passe.

XVIII-A-2 - Créer une migration

On va encore utiliser l'outil de migration pour créer ces tables (vous n'êtes évidemment pas obligé de l'utiliser). Commencez par créer une base nommée blog dans MySQL (si vous utilisez un autre serveur ce qui suit reste évidemment valable, par contre il faudra peut-être adapter certains types de données dans les tables). Ensuite créez une migration :

Maintenant installez cette migration :

Vous devez avoir une table migrations dans votre base :

XVIII-A-3 - Le schéma des tables

Dans le dossier app/database/migrations vous devez avoir un fichier avec ce code :

1. <?php

3. use Illuminate\Database\Schema\Blueprint;

4. use Illuminate\Database\Migrations\Migration;

6. class CreateBlog extends Migration {

8. /**

9. * Run the migrations.

10. *

11. * @return void

12. */

13. public function up()

14. {

15. //

16. }

18. /**

19. * Reverse the migrations.

20. *

21. * @return void

22. */

23. public function down()

24. {

25. //

26. }

28. }

Il ne nous reste plus qu'à écrire le reste. Nous avons déjà vu ça au chapitre 133. Mais à présent voyons un cas plus réaliste et complet. Voici le code :

1. <?php

3. use Illuminate\Database\Schema\Blueprint;

4. use Illuminate\Database\Migrations\Migration;

6. class CreateBlog extends Migration {

8. public function up()

9. {

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

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

12. $table->string('username', 64)->unique();

13. $table->string('password', 64);

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

15. $table->enum('statut', array('user', 'admin'))->default('user');

16. $table->string('remember_token', 100)->nullable();

17. $table->timestamps();

18. });

19. Schema::create('categories', function($table) {

20. $table->increments('id')->unsigned();

21. $table->string('title', 128)->unique();

22. $table->text('description')->nullable();

23. $table->timestamps();

24. });

25. Schema::create('articles', function($table) {

26. $table->increments('id')->unsigned();

27. $table->string('title', 128);

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

29. $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade');

30. $table->integer('categorie_id')->unsigned();

31. $table->foreign('categorie_id')->references('id')->on('categories')->onDelete('cascade')->onUpdate('cascade');

32. $table->text('intro_text');

33. $table->text('full_text');

34. $table->enum('allow_comment', array('no', 'yes'))->default('yes');

35. $table->timestamps();

36. });

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

38. $table->increments('id')->unsigned();

39. $table->string('title', 128);

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

41. $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade');

42. $table->integer('article_id')->unsigned();

43. $table->foreign('article_id')->references('id')->on('articles')->onDelete('cascade')->onUpdate('cascade');

44. $table->text('text');

45. $table->timestamps();

46. });

47. }

49. public function down()

50. {

51. Schema::table('articles', function($table) {

52. $table->dropForeign('articles_categorie_id_foreign');

53. $table->dropForeign('articles_user_id_foreign');

54. });

55. Schema::table('comments', function($table) {

56. $table->dropForeign('comments_user_id_foreign');

57. $table->dropForeign('comments_article_id_foreign');

58. });

59. Schema::drop('categories');

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

61. Schema::drop('comments');

62. Schema::drop('articles');

63. }

65. }

Il ne reste plus qu'à lancer la migration :

Vous devez avoir vos tables dans la base :

Pour le détail de la syntaxe du schéma vous pouvez consulter la documentation. Remarquez aussi que pour la suppression des tables, j'ai commencé par supprimer les clés étrangères pour éviter de rencontrer des erreurs dans MySQL.

XVIII-A-4 - Des données

Nous allons avoir besoin de quelques données pour tester notre site. Nous allons utiliser une fonctionnalité déjà vue avec les seeds. Il faut créer un fichier par table, en donnant à chacun un nom judicieux et les placer dans le dossier app/database/seeds. Pour la table users on aura donc le fichier UserTableSeeder.php :

1. <?php

2. class UserTableSeeder extends Seeder {

4. public function run()

5. {

6. DB::table('users')->insert(

8. array(

9. array(

10. 'id' => 1,

11. 'username' => 'admin',

12. 'password' => Hash::make('admin'),

13. 'email' => 'admin@plop.fr',

14. 'statut' => 'admin',

15. 'created_at' => new DateTime,

16. 'updated_at' => new DateTime,

17. ),

19. array(

20. 'id' => 2,

21. 'username' => 'Dupont',

22. 'password' => Hash::make('dupont'),

23. 'email' => 'dupont@plop.fr',

24. 'statut' => 'user',

25. 'created_at' => new DateTime,

26. 'updated_at' => new DateTime,

27. ),

29. array(

30. 'id' => 3,

31. 'username' => 'Durand',

32. 'password' => Hash::make('durand'),

33. 'email' => 'durand@plop.fr',

34. 'statut' => 'user',

35. 'created_at' => new DateTime,

36. 'updated_at' => new DateTime,

37. )

38. )

39. );

40. }

41. }

Ici on crée trois utilisateurs dont un administrateur. On crée aussi un fichier CategorieTableSeeder.php pour les catégories :

1. <?php

3. class CategorieTableSeeder extends Seeder {

5. public function run()

6. {

7. DB::table('categories')->insert(

9. array (

10. array(

11. 'id' => 1,

12. 'title' => 'Catégorie 1',

13. 'description' => 'blablabla',

14. 'created_at' => new DateTime,

15. 'updated_at' => new DateTime

16. ),

17. array(

18. 'id' => 2,

19. 'title' => 'Catégorie 2',

20. 'description' => 'blablabla',

21. 'created_at' => new DateTime,

22. 'updated_at' => new DateTime

23. ),

24. array(

25. 'id' => 3,

26. 'title' => 'Catégorie 3',

27. 'description' => 'blablabla',

28. 'created_at' => new DateTime,

29. 'updated_at' => new DateTime

30. ),

31. array(

32. 'id' => 4,

33. 'title' => 'Catégorie 4',

34. 'description' => 'blablabla',

35. 'created_at' => new DateTime,

36. 'updated_at' => new DateTime

37. )

38. )

39. );

40. }

42. }

On crée aussi un fichier ArticleTableSeeder.php pour les articles :

1. <?php

3. class ArticleTableSeeder extends Seeder {

5. public function run()

6. {

7. DB::table('articles')->insert(

9. array (

10. array(

11. 'id' => 1,

12. 'title' => 'Article 1',

13. 'user_id' => '1',

14. 'categorie_id' => '1',

15. 'intro_text' => 'Intro 1',

16. 'full_text' => 'blablabla',

17. 'created_at' => '2013-02-01 00:00:00',

18. 'updated_at' => '2013-02-01 00:00:00'

19. ),

20. array(

21. 'id' => 2,

22. 'title' => 'Article 2',

23. 'user_id' => '1',

24. 'categorie_id' => '1',

25. 'intro_text' => 'Intro 2',

26. 'full_text' => 'blablabla',

27. 'created_at' => '2013-02-02 00:00:00',

28. 'updated_at' => '2013-02-02 00:00:00'

29. ),

30. array(

31. 'id' => 3,

32. 'title' => 'Article 3',

33. 'user_id' => '1',

34. 'categorie_id' => '2',

35. 'intro_text' => 'Intro 3',

36. 'full_text' => 'blablabla',

37. 'created_at' => '2013-02-06 00:00:00',

38. 'updated_at' => '2013-02-06 00:00:00'

39. ),

40. array(

41. 'id' => 4,

42. 'title' => 'Article 4',

43. 'user_id' => '1',

44. 'categorie_id' => '2',

45. 'intro_text' => 'Intro 4',

46. 'full_text' => 'blablabla',

47. 'created_at' => '2013-02-05 00:00:00',

48. 'updated_at' => '2013-02-05 00:00:00'

49. ),

50. array(

51. 'id' => 5,

52. 'title' => 'Article 5',

53. 'user_id' => '1',

54. 'categorie_id' => '3',

55. 'intro_text' => 'Intro 5',

56. 'full_text' => 'blablabla',

57. 'created_at' => '2013-01-01 00:00:00',

58. 'updated_at' => '2013-01-01 00:00:00'

59. ),

60. array(

61. 'id' => 6,

62. 'title' => 'Article 6',

63. 'user_id' => '1',

64. 'categorie_id' => '4',

65. 'intro_text' => 'Intro 6',

66. 'full_text' => 'blablabla',

67. 'created_at' => '2013-02-04 00:00:00',

68. 'updated_at' => '2013-02-04 00:00:00'

69. )

70. )

72. );

73. }

75. }

Et on crée un dernier pour les commentaires CommentTableSeeder.php :

1. <?php

3. class CommentTableSeeder extends Seeder {

5. public function run()

6. {

7. DB::table('comments')->insert(

9. array(

10. array(

11. 'id' => 1,

12. 'title' => 'Commentaire 1',

13. 'user_id' => '2',

14. 'article_id' => '1',

15. 'text' => 'blablabla',

16. 'created_at' => '2013-02-01 00:00:00',

17. 'updated_at' => '2013-02-01 00:00:00'

18. ),

19. array(

20. 'id' => 2,

21. 'title' => 'Commentaire 2',

22. 'user_id' => '2',

23. 'article_id' => '1',

24. 'text' => 'blablabla',

25. 'created_at' => '2013-02-01 00:00:00',

26. 'updated_at' => '2013-02-01 00:00:00'

27. ),

28. array(

29. 'id' => 3,

30. 'title' => 'Commentaire 3',

31. 'user_id' => '3',

32. 'article_id' => '3',

33. 'text' => 'blablabla',

34. 'created_at' => '2013-02-01 00:00:00',

35. 'updated_at' => '2013-02-01 00:00:00'

36. ),

37. array(

38. 'id' => 4,

39. 'title' => 'Commentaire 4',

40. 'user_id' => '2',

41. 'article_id' => '4',

42. 'text' => 'blablabla',

43. 'created_at' => '2013-02-01 00:00:00',

44. 'updated_at' => '2013-02-01 00:00:00'

45. ),

46. array(

47. 'id' => 5,

48. 'title' => 'Commentaire 5',

49. 'user_id' => '3',

50. 'article_id' => '4',

51. 'text' => 'blablabla',

52. 'created_at' => '2013-02-01 00:00:00',

53. 'updated_at' => '2013-02-01 00:00:00'

54. ),

55. array(

56. 'id' => 6,

57. 'title' => 'Commentaire 6',

58. 'user_id' => '3',

59. 'article_id' => '5',

60. 'text' => 'blablabla',

61. 'created_at' => '2013-02-01 00:00:00',

62. 'updated_at' => '2013-02-01 00:00:00'

63. ),

64. array(

65. 'id' => 7,

66. 'title' => 'Commentaire 7',

67. 'user_id' => '2',

68. 'article_id' => '1',

69. 'text' => 'blablabla',

70. 'created_at' => '2013-02-01 00:00:00',

71. 'updated_at' => '2013-02-01 00:00:00'

72. )

73. )

74. );

75. }

77. }

Et il faut renseigner le fichier DatabaseSeeder.php en appelant nos classes dans le bon ordre pour ne pas rencontrer des exceptions de la part de MySQL :

1. <?php

3. class DatabaseSeeder extends Seeder {

5. public function run()

6. {

7. $this->call('UserTableSeeder');

8. $this->call('CategorieTableSeeder');

9. $this->call('ArticleTableSeeder');

10. $this->call('CommentTableSeeder');

11. }

13. }

Il ne reste plus qu'à utiliser Artisan :

XVIII-A-5 - Les modèles

Nous avons vu en étudiant l'authentification que Laravel possède déjà un modèle pour les utilisateurs. Il nous faut compléter ce modèle pour faire fonctionner nos relations. Voici donc le fichier app/models/User complété :

1. <?php

3. use Illuminate\Auth\UserInterface;

4. use Illuminate\Auth\Reminders\RemindableInterface;

6. class User extends Eloquent implements UserInterface, RemindableInterface {

8. protected $table = 'users';

10. protected $hidden = array('password');

12. public function getAuthIdentifier()

13. {

14. return $this->getKey();

15. }

17. public function getAuthPassword()

18. {

19. return $this->password;

20. }

22. public function getRememberToken()

23. {

24. return $this->remember_token;

25. }

27. public function setRememberToken($value)

28. {

29. $this->remember_token = $value;

30. }

32. public function getRememberTokenName()

33. {

34. return 'remember_token';

35. }

37. public function getReminderEmail()

38. {

39. return $this->email;

40. }

42. public function articles()

43. {

44. return $this->hasMany('Article');

45. }

47. public function comments()

48. {

49. return $this->hasMany('Comment');

50. }

52. }

Nous avons également besoin d'un modèle pour les catégories (app/models/Categorie) :

1. class Categorie extends Eloquent {

3. public function articles()

4. {

5. return $this->hasMany('Article');

6. }

7. }

Un autre pour les articles (app/models/Article) :

1. class Article extends Eloquent {

3. public function comments()

4. {

5. return $this->hasMany('Comment');

6. }

8. public function categorie()

9. {

10. return $this->belongsTo('Categorie');

11. }

13. public function user()

14. {

15. return $this->belongsTo('User');

16. }

17. }

Et un dernier modèle pour les commentaires (app/models/Comment) :

1. class Comment extends Eloquent {

3. public function article()

4. {

5. return $this->belongsTo('Article');

6. }

8. public function user()

9. {

10. return $this->belongsTo('User');

11. }

12. }

XVIII-B - Migration pour l'authentification

On peut créer automatiquement une migration en utilisant Artisan :

Ce qui a pour effet de créer la migration :

Avec ce code :

1. <?php

3. use Illuminate\Database\Schema\Blueprint;

4. use Illuminate\Database\Migrations\Migration;

6. class CreatePasswordRemindersTable extends Migration {

8. /**

9. * Run the migrations.

10. *

11. * @return void

12. */

13. public function up()

14. {

15. Schema::create('password_reminders', function(Blueprint $table)

16. {

17. $table->string('email')->index();

18. $table->string('token')->index();

19. $table->timestamp('created_at');

20. });

21. }

23. /**

24. * Reverse the migrations.

25. *

26. * @return void

27. */

28. public function down()

29. {

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

31. }

33. }

Il nous faut finalement lancer la migration pour créer cette table dans la base :

Nous avons à présent tout ce qui nous est nécessaire pour stocker et manipuler les données. La prochaine étape sera de définir l'aspect du blog.

XIX - Chapitre 19 : Un blog : le template et les routes

XIX-A - L'aspect du blog

En général pour réaliser un site, je commence par faire une page HTML de l'aspect qu'il aura pour fixer la mise en page et la feuille de style. Pour ce blog on va faire simple et utiliser Bootstrap de Twitter pour simplifier le code. On va avoir un entête avec le nom du blog, une barre de navigation qui comportera les catégories et un espace pour le contenu, et enfin un pied de page sommaire :

Voici le code HTML de la page :

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

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

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

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

7. <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css">

8. <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap-theme.min.css">

9. <link type="text/css" rel="stylesheet" href="assets/css/main.css" >

10. <link type="text/css" rel="stylesheet" href="http://fonts.googleapis.com/css?family=Imprima" >

11. </head>

13. <body>

14. <div class="container">

15. <header class="jumbotron" id="entete">

16. <h1>Mon joli blog !</h1>

17. </header>

18. <nav class="navbar navbar-default">

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

20. <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">

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

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

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

24. </button>

25. <a class="navbar-brand" href="#">Mon joli blog</a>

26. </div>

27. <div class="collapse navbar-collapse">

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

29. <li><a href="#" class="actif">Accueil</a></li>

30. <li><a href="#">Catégorie 1</a></li>

31. <li><a href="#">Catégorie 2</a></li>

32. <li><a href="#">Catégorie 3</a></li>

33. <li><a href="#">Catégorie 4</a></li>

34. </ul>

35. <form class="navbar-form navbar-left pull-right">

36. <input type="text" class="form-group form-control" placeholder="Recherche">

37. </form>

38. </div>

39. </nav>

40. <section class="row">

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

42. <h2>Titre</h2>

43. <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>

44. <a href="#" class="btn btn-default">Lire la suite <span class="glyphicon glyphicon-play"></span></a>

45. </div>

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

47. <h2>Titre</h2>

48. <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>

49. <a href="#" class="btn btn-default">Lire la suite <span class="glyphicon glyphicon-play"></span></a>

50. </div>

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

52. <h2>Titre</h2>

53. <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>

54. <a href="#" class="btn btn-default">Lire la suite <span class="glyphicon glyphicon-play"></span></a>

55. </div>

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

57. <h2>Titre</h2>

58. <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>

59. <a href="#" class="btn btn-default">Lire la suite <span class="glyphicon glyphicon-play"></span></a>

60. </div>

61. </section>

62. <footer>

63. <em>© 2013</em>

64. </footer>

65. </div>

66. <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>

67. <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.2/js/bootstrap.min.js"></script>

68. </body>

J'utilise des CDN pour les bibliothèques nécessaires. Et voici la feuille de style main.css :

1. body {

2. font-family: 'Imprima', sans-serif;

3. background-color: #F5F5F5;

4. }

5. .container {

6. margin-top: 20px;

7. }

8. .jumbotron, .navbar, .comment {

9. box-shadow: 2px 2px 6px #bbb;

10. }

11. .comment {

12. border:thin;

13. border-style:solid;

14. border-color:#bbb;

15. padding: 10px;

16. margin-bottom: 10px;

17. border-radius: 10px;

18. background-color:#ddd;

19. }

20. textarea {

21. width:98%;

22. }

23. .nav li a {

24. font-size:large;

25. }

26. .nav li a:not(.actif):hover {

27. background-color:#eee !important;

28. box-shadow: 2px 2px 14px #555;

29. }

30. .nav li a.actif {

31. background-color:#888 !important;

32. color:#fff !important;

33. box-shadow: 2px 2px 14px #555;

34. }

35. footer {

36. margin-top: 20px;

37. text-align: center;

38. }

Je ne commente pas les éléments HTML et CSS qui sortent du cadre de Laravel. Les options de la barre de navigation seront créées dynamiquement (à part Accueil) en fonction des catégories présentes, auxquelles il faudra sans doute adjoindre un lien pour la connexion/déconnexion.

XIX-A-1 - Le template

Maintenant il nous faut transformer notre page HTML en template utilisable. Nous allons évidemment faire appel à Blade dont je vous ai déjà parlé. On va conserver dans la page les informations fixes et créer des sections pour les parties qui doivent changer.

On trouve deux sections, une pour la navigation et une autre pour le contenu. On crée un fichier app/views/template_blog.blade.php :

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

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

5. <title>

6. Mon joli blog

7. </title>

8. <meta name="viewport" content="width=device-width, initial-scale=1.0">

9. {{ HTML::style('//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css') }}

10. {{ HTML::style('//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap-theme.min.css') }}

11. {{ HTML::style('assets/css/main.css') }}

12. {{ HTML::style('http://fonts.googleapis.com/css?family=Imprima') }}

13. </head>

15. <body>

16. <div class="container">

18. <header class="jumbotron" id="entete">

19. <h1>Mon joli blog !</h1>

20. </header>

22. <nav class="navbar navbar-default">

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

24. <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">

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

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

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

28. </button>

29. <a class="navbar-brand" href="#">Mon joli blog</a>

30. </div>

31. <div class="collapse navbar-collapse">

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

33. @yield('navigation')

34. </ul>

35. {{ Form::open(array('url' => 'find', 'method' => 'POST', 'class' => 'navbar-form navbar-left pull-right')) }}

36. {{ Form::text('find', '', array('class' => 'form-group form-control', 'placeholder' => 'Recherche')) }}

37. {{ Form::close() }}

38. </div>

39. </nav>

41. <section class="row">

42. @yield('content')

43. </section>

45. <footer>

46. <em>© 2013</em>

47. </footer>

49. </div>

50. {{ HTML::script('//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js'); }}

51. {{ HTML::script('//netdna.bootstrapcdn.com/bootstrap/3.0.2/js/bootstrap.min.js'); }}

52. </body>

53. </html>

Les liens pour les feuilles de style se font avec la méthode style de la classe HTML (si ça ne fonctionne pas, assurez-vous d'avoir la dernière version du framework avec composer update).

Maintenant il nous faut une vue pour exploiter ce template. Mais nous devons envisager deux situations différentes : si on affiche la page d'accueil ou une catégorie on va avoir plusieurs introductions d'articles affichées alors que si on affiche un article nous avons juste le titre et le texte de cet article avec ses commentaires et peut-être d'autres informations que nous pourrons greffer ultérieurement. Il semble donc judicieux de créer une vue spécifique pour la navigation qui sera toujours traitée de la même façon, et deux vues à appeler directement : une pour une catégorie ou l'accueil, et une autre pour un article.

Pour la navigation il nous faudra recevoir les informations des catégories dans une variable $categories et l'index de la catégorie active dans $actif, ce qui donne le fichier app/views/navigation.blade.php :

1. @section('navigation')

3. <li>{{ link_to_route('accueil', 'Accueil', null, ($actif == 0)? array('class' => 'actif'): null) }}</li>

5. @foreach ($categories as $categorie)

6. <li>{{ link_to('cat/'.$categorie->id, $categorie->title, ($actif == $categorie->id)? array('class' => 'actif'): null) }}</li>

7. @endforeach

9. @stop

La vue pour l'accueil et l'affichage des catégories doit mentionner évidemment le layout qu'on a créé et aussi intégrer la vue pour la navigation. On reçoit les informations des articles dans la variable $articles. Ce qui donne le fichier app/views/accueil.blade.php :

1. @extends('template_blog')

3. @include('navigation')

5. @section('content')

7. @for ($i = 0; $i < count($articles); $i++)

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

9. <h3>{{$articles[$i]->title}}</h3>

10. <p>{{$articles[$i]->intro_text}}</p>

11. <a class="btn btn-default" href="{{ url('art/'.$actif.'/'.$articles[$i]->id) }}">Lire la suite <span class="glyphicon glyphicon-play"></span></a>

12. </div>

13. @endfor

15. @stop

Il ne nous manque plus que la vue pour un article app/views/article.blade.php :

1. @extends('template_blog')

3. @include('navigation')

5. @section('content')

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

8. <div class="well">

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

10. Ecrit le {{date('d-m-Y',strtotime($article->created_at))}}

11. </em>

12. <h3>

13. {{$article->title}}

14. </h3>

15. <p>

16. {{$article->full_text}}

17. </p>

18. </div>

19. </div>

21. @foreach ($comments as $comment)

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

23. <div class="comment">

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

25. Ecrit par {{$comment->username}} le {{date('d-m-Y', strtotime($comment->created_at))}}

26. </em>

27. <h5>

28. {{$comment->title}}

29. </h5>

30. <p>

31. {{$comment->text}}

32. </p>

33. </div>

34. </div>

35. @endforeach

37. @if ($article->allow_comment and Auth::check())

38. <hr />

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

40. <div class="well">

41. {{ Form::open(array('url' => 'comment')) }}

42. {{ Form::hidden('id_art', $article->id) }}

43. {{ Form::hidden('id_cat', $actif) }}

44. {{ Form::label('title', 'Titre de votre commentaire :') }}

45. {{ Form::text('title', '', $attributes = array('class' => 'form-control')) }}

46. {{ Form::label('comment', 'Votre commentaire :') }}

47. {{ Form::textarea('comment', '', $attributes = array('class' => 'form-control')) }}

48. {{ Form::submit('Envoyer', array('class' => 'btn btn-default')) }}

49. {{ Form::close() }}

50. </div>

51. </div>

52. @endif

54. @stop

Remarquez encore l'utilisation de la classe form pour le formulaire ainsi que les deux conditions pour afficher ce formulaire de saisie de commentaire : allow_comment doit être vrai et l'utilisateur doit être authentifié, ce qui nous est donné par Auth::check(). Pour tester il nous faudra dans la route correspondante authentifier artificiellement un utilisateur dans un premier temps.

XIX-A-2 - Les routes

On va créer les routes et quelques informations pour tester tout ça. Le code correspondant sera évidemment dans app/routes.php. Il nous faut déjà une première route pour répondre à l'URI http://localhost/blog/public. Au niveau des données nous avons besoin de connaître les catégories existantes pour la barre de navigation ainsi que les titres et introductions des articles qui vont figurer sur la page d'accueil. Comme on n'a pas prévu de pagination on va se retrouver avec tous les articles listés, il nous faudra évidemment corriger cela plus tard mais pour le moment nous allons nous en contenter. Voici donc la syntaxe de cette route :

1. Route::get('/', array('as' => 'accueil', function()

2. {

3. $categories = Categorie::all();

4. $articles = Article::select('id', 'title', 'intro_text')->orderBy('created_at', 'desc')->get();

5. return View::make('accueil', array('categories' => $categories, 'articles' => $articles, 'actif' => 0));

6. }));

On peut déjà tester cette route :

On obtient bien le résultat attendu . Continuons avec la route quand on clique sur une catégorie. Cette fois l'URI est du genre http://localhost/blog/public/cat/1. Il nous faut récupérer dans la base tous les articles de cette catégorie (on n'a pas non plus prévu ici de pagination). On va aussi vérifier que l'index de la catégorie correspond à une donnée existante, dans le cas contraire on déclenche une erreur 404 :

1. Route::get('cat/{id}', function($id)

2. {

3. $categories = Categorie::all();

4. $categorie = Categorie::find($id);

5. if($categorie) {

6. $articles = $categorie->articles()->orderBy('created_at', 'desc')->get();

7. return View::make('accueil', array('categories' => $categories, 'articles' => $articles, 'actif' => $id));

8. }

9. else App::abort(404);

10. });

11. App::missing(function($exception)

12. {

13. return 'Cette page n\'existe pas !';

14. });

On vérifie aussi que ça fonctionne, par exemple en cliquant sur la première catégorie :

Il ne nous manque plus que la route pour l'affichage d'un article du genre http://localhost/blog/public/art/1/1. On va chercher dans la base toutes les données de cet article. Au passage on vérifie que les index transmis correspondent à des données sinon on déclenche une erreur 404 :

1. Route::get('art/{cat_id}/{art_id}', function($cat_id, $art_id)

2. {

3. $categories = Categorie::all();

4. $article = Article::find($art_id);

5. if($article) {

6. //////////////////////////////////////////////////////////////

7. // Log d'un utilisateur pour tester la saisie des commentaires

8. $user = User::where('username', '=', 'admin')->first();

9. Auth::login($user);

10. //////////////////////////////////////////////////////////////

11. $comments = $article->comments()->orderBy('comments.created_at', 'desc')

12. ->join('users', 'users.id', '=', 'comments.user_id')

13. ->select('users.username', 'title', 'text', 'comments.created_at')

14. ->get();

15. return View::make('article', array('categories' => $categories, 'article' => $article, 'actif' => $cat_id, 'comments' => $comments));

16. }

17. else App::abort(404);

18. });

J'ai aussi prévu l'authentification de l'administrateur pour voir apparaître le formulaire de saisie des commentaires. On supprimera ces lignes quand on aura vérifié que tout fonctionne correctement. Voici le résultat avec l'URI mentionné ci-dessus :

L'affichage est terminé, il ne nous manque plus que la route pour recevoir le retour de la saisie d'un commentaire, il faut enregistrer les valeurs dans la tables comments et rediriger sur la page de l'article :

1. Route::post('comment', array('before' => 'csrf', function()

2. {

3. Comment::create(

4. array(

5. 'title' => Input::get('title'),

6. 'user_id' => Auth::user()->id,

7. 'article_id' => Input::get('id_art'),

8. 'text' => Input::get('comment')

9. )

10. );

11. return Redirect::to('art/'.Input::get('id_cat').'/'.Input::get('id_art'));

12. }));

Cette fois on a un type post. Notez que tel qu'est écrit ce code, l'utilisateur peut transmettre des balises HTML ; pour des raisons de sécurité vous pouvez filtrer les entrées, par exemple avec la fonction strip_tags(). Vous pouvez aussi autoriser certaines balises en filtrant les entrées ou encore mieux utiliser HTML Purifier (il y a un paquetage pour l'intégrer à Laravel 4). On a aussi un contrôle csrf pour la sécurité avec un filtre sur la route.

Les modèles de Laravel prévoient désormais une protection d'assignation de masse, et si vous utilisez le code ci-dessus vous allez rencontrer cette exception :

Pour éviter ça, vous devez déclarer les champs autorisés dans le modèle app/model/Comment.php :

1. class Comment extends Eloquent {

3. protected $fillable = array('title', 'user_id', 'article_id', 'text');

5. public function article()

6. {

7. return $this->belongsTo('Article');

8. }

10. public function user()

11. {

12. return $this->belongsTo('User');

13. }

14. }

XIX-A-3 - La pagination

Ceux qui ont déjà codé une pagination de pages Web savent que ça peut être assez laborieux. Avec Laravel c'est presque trop facile. Malheureusement la version « automatique » ne connaît pour le moment que Bootstrap 2, alors il va falloir un peu besogner pour installer la dernière version. Ouvrez le fichier app/config/view.php et trouvez cette ligne :

'pagination' => 'pagination::slider',

Modifiez-la pour obtenir ça :

'pagination' => 'pagination::slider-3',

Maintenant votre pagination est « Bootstrap 3 ready » ! Je vais vous montrer comment ajouter cette pagination à la page d'accueil. Apportez cette modification à la route de l'accueil :

1. Route::get('/', array('as' => 'accueil', function()

2. {

3. $categories = Categorie::all();

4. $articles = Article::select('id', 'title', 'intro_text')->orderBy('created_at', 'desc')->paginate(4);

5. return View::make('accueil', array('categories' => $categories, 'articles' => $articles, 'actif' => 0));

6. }));

J'ai remplacé get() par paginate(4) pour obtenir quatre articles à la fois. Il faut aussi créer les liens de navigation dans la vue app/views/accueil.blade.php :

1. @extends('template_blog')

3. @include('navigation')

5. @section('content')

7. @for ($i = 0; $i < count($articles); $i++)

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

9. <h3>{{$articles[$i]->title}}</h3>

10. <p>{{$articles[$i]->intro_text}}</p>

11. <a class="btn btn-default" href="{{ url('art/'.$actif.'/'.$articles[$i]->id) }}">Lire la suite <span class="glyphicon glyphicon-play"></span></a>

12. </div>

13. @endfor

15. @if (method_exists($articles,'links'))

16. {{$articles->links()}}

17. @endif

19. @stop

Et voilà le résultat :

Notez que la mise en forme est automatiquement adaptée à l'utilisation de Bootstrap !

Il nous faut faire la même chose pour la route des catégories :

1. Route::get('cat/{id}', function($id)

2. {

3. $categories = Categorie::all();

4. $categorie = Categorie::find($id);

5. if($categorie) {

6. $articles = $categorie->articles()->orderBy('created_at', 'desc')->paginate(4);

7. return View::make('accueil', array('categories' => $categories, 'articles' => $articles, 'actif' => $id));

8. }

9. else App::abort(404);

10. });

XIX-A-4 - Injecter l'instance d'un modèle

Il est possible de lier un modèle à une route, autrement dit de définir le paramètre transmis comme étant l'id d'un enregistrement correspondant au modèle, et d'injecter l'objet correspondant. Nous allons donc optimiser nos routes avec cette élégante possibilité. Reprenons dans un premier temps la route pour les catégories :

1. Route::model('cat', 'Categorie');

2. Route::get('cat/{cat}', function(Categorie $categorie)

3. {

4. $articles = $categorie->articles()->orderBy('created_at', 'desc')->paginate(4);

5. return View::make('accueil', array('categories' => Categorie::all(), 'articles' => $articles, 'actif' => $categorie->id));

6. });

On n'a même plus besoin de tester pour voir si la catégorie existe bien, Laravel se charge lui-même d'envoyer une erreur 404 ! On va utiliser la même possibilité pour les articles :

1. Route::model('art', 'Article');

2. Route::get('art/{cat_id}/{art}', function($cat_id, Article $article)

3. {

4. $comments = $article->comments()->orderBy('comments.created_at', 'desc')

5. ->join('users', 'users.id', '=', 'comments.user_id')

6. ->select('users.username', 'title', 'text', 'comments.created_at')

7. ->get();

8. return View::make('article', array('categories' => Categorie::all(), 'article' => $article, 'actif' => $cat_id, 'comments' => $comments));

9. });

XIX-A-5 - Le formulaire de recherche

Il nous faut aussi faire fonctionner le formulaire de recherche. On prévoit une route pour lui :

1. Route::post('find', array('before' => 'csrf', function() {

2. $match = Input::get('find');

3. if($match) {

4. return Redirect::route('accueil', array('match' => $match))

5. ->with('flash_notice', 'Résultats pour la recherche du terme '.$match);

6. } else {

7. return Redirect::route('accueil')

8. ->with('flash_error', 'Il faudrait entrer un terme pour la recherche !');

9. }

10. }));

J'ai prévu la transmission d'informations en variables de session « flash », qui ne servent donc que pour cette redirection. Voilà déjà la route de l'accueil modifiée :

1. Route::get('/{match?}', array('as' => 'accueil', function ($match = NULL) {

2. if($match) {

3. $articles = Article::select('id', 'title', 'intro_text')

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

5. ->where('intro_text', 'like', '%'.$match.'%')

6. ->orwhere('full_text', 'like', '%'.$match.'%')

7. ->get();

8. } else {

9. $articles = Article::select('id', 'title', 'intro_text')

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

11. ->paginate(4);

12. }

13. return View::make('accueil',  array(

14. 'categories' => Categorie::all(),

15. 'articles' => $articles,

16. 'actif' => 0

17. ));

18. }));

J'ai ajouté un paramètre optionnel pour recevoir le terme de recherche (attention toutefois à ce type de route qui va intercepter tous les URI comportant un seul paramètre ! Dans le cas de ce blog le problème ne se présente pas, sinon il faudrait placer cette route en dernier. J'ai aussi ajouté la requête correspondante. Il ne nous reste plus qu'à adapter la vue de l'accueil pour recevoir les informations de session et aussi pour faire un test pour les liens de pagination. En effet il serait délicat ici de faire une pagination pour le résultat de la recherche :

1. @extends('template_blog')

3. @include('navigation')

5. @section('content')

7. @if (Session::has('flash_notice'))

8. <div class="alert alert-success">

9. {{ Session::get('flash_notice') }}

10. </div>

11. @endif

13. @if (Session::has('flash_error'))

14. <div class="alert alert-danger">

15. {{ Session::get('flash_error') }}

16. </div>

17. @endif

19. @for ($i = 0; $i < count($articles); $i++)

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

21. <h3>{{$articles[$i]->title}}</h3>

22. <p>{{$articles[$i]->intro_text}}</p>

23. <a class="btn btn-default" href="{{ url('art/'.$actif.'/'.$articles[$i]->id) }}">Lire la suite <span class="glyphicon glyphicon-play"></span></a>

24. </div>

25. @endfor

27. @if (method_exists($articles,'links'))

28. {{$articles->links()}}

29. @endif

31. @stop

Il ne nous reste plus qu'à tester ça :

Et si on ne rentre aucun terme de recherche :

XIX-A-6 - Le groupement des filtres

Une dernière touche d'optimisation. On va grouper deux routes qui utilisent le même filtre :

1. Route::group(array('before' => 'csrf'), function () {

3. Route::post('comment', function() {

4. Comment::create(

5. array(

6. 'title' => Input::get('title'),

7. 'user_id' => Auth::user()->id,

8. 'article_id' => Input::get('id_art'),

9. 'text' => Input::get('comment')

10. )

11. );

12. return Redirect::to('art/'.Input::get('id_cat').'/'.Input::get('id_art'));

13. });

15. Route::post('find', function() {

16. $match = Input::get('find');

17. if($match) {

18. return Redirect::route('accueil', array('match' => $match))

19. ->with('flash_notice', 'Résultats pour la recherche du terme '.$match);

20. } else {

21. return Redirect::route('accueil')

22. ->with('flash_error', 'Il faudrait entrer un terme pour la recherche !');

23. }

24. });

26. });

Et voilà le travail . Notre blog commence a avoir un peu d'allure mais il nous reste du travail. Si vous êtes un peu perdu dans le code vous pouvez télécharger les fichiers correspondant à ce chapitre et au précédent. La prochaine étape sera l'authentification des utilisateurs.

XX - Chapitre 20 : Quel IDE pour Laravel 4 ?

L'organisation du code dans Laravel présente un défi aux IDE (Environnement De Développement) et finalement je n'en ai trouvé qu'un seul à même de le relever, Sublime Text. C'est en plus un IDE simple, puissant, esthétique. Mais il nécessite quelques préparations pour être pleinement opérationnel avec Laravel 4. Commencez par le télécharger et l'installer. Vous pourrez déjà constater qu'il se débrouille très bien au niveau intelliSense :

Mais on va encore améliorer ça en complétant Sublime Text. On va commencer par installer le Package Control. Suivez les indications données sur le site pour l'ajouter à l'IDE. Il suffit d'ouvrir la console avec le menu :

Et dans la console vous entrez le code figurant sur la page d'installation :

On vous demande ensuite de redémarrer Sublime Text :

Donc vous le relancez. Qu'avez-vous obtenu ? Pour le voir tapez CTRL MAJ P pour ouvrir la palette de commande. Et là choisissez l'option Install Package :

Dans la liste des paquetages disponibles choisissez SublimeCodeIntel :

Attendez la fin de l'installation :

Il ne vous reste plus qu'à aller chercher le fichier _ide_helpers.php et de l'ajouter dans le répertoire app de votre application Laravel https://github.com/laravelbook/laravel4-sublimetext-helper/blob/master/_ide_helpers.php

Relancez encore une fois Sublime Text et maintenant développez Laravel avec plus de facilité :

Découvrons une autre fonctionnalité pratique. Positionnez le curseur par exemple au-dessus d'un Route et cliquez en maintenant la touche ALT enfoncée :

Vous êtes renvoyé vers la bonne ligne du fichier _ide_helper_.php avec la description des méthodes disponibles pour cette classe.

Sublime Text n'est pas un logiciel gratuit mais il n'y a pas de limitation à la durée d'essai, alors si vous l'utilisez un certain temps soutenez son développement.

XXI - Chapitre 21 : Un blog : utilisation de contrôleurs

Nous allons continuer l'application « blog » en essayant de mieux organiser le code. Dans la précédente étape nous avons utilisé les routes pour effectuer tout le traitement. Ça fonctionne correctement mais ce n'est pas très lisible. Alors je vous propose d'utiliser un contrôleur pour faire le traitement et ne conserver dans les routes que l'aiguillage de requêtes.

XXI-A - Les routes

Le fichier des routes ne conserve que ce pour quoi il est fait, le routage :

1. <?php

2. Route::get('/', array('uses' => 'HomeController@accueil', 'as' => 'accueil'));

4. Route::model('cat', 'Categorie');

5. Route::get('cat/{cat}', 'HomeController@categorie');

7. Route::model('art', 'Article');

8. Route::get('art/{cat_id}/{art}', 'HomeController@article');

10. Route::post('find', 'HomeController@find');

11. Route::post('comment', 'HomeController@comment');

J'ai prévu de nommer une seule route, pour l'accueil ; je trouve que le code est ensuite plus lisible et ça permet de montrer comment on nomme une route vers un contrôleur par la même occasion. J'ai conservé l'injection d'instance des modèles Categorie et Article qui rend la syntaxe plus légère.

XXI-B - Le contrôleur

Le contrôleur HomeController est chargé de toutes les actions pour les articles :

1. <?php

3. class HomeController extends BaseController {

5. public function __construct()

6. {

7. $this->beforeFilter('csrf', ['on' => 'post']);

8. }

10. /**

11. * Génère la vue de l'accueil

12. *

13. * @param $articles

14. * @return View

15. */

16. private function gen_accueil($articles)

17. {

18. return View::make('accueil',  array(

19. 'categories' => Categorie::all(),

20. 'articles' => $articles,

21. 'actif' => 0

22. ));

23. }

25. /**

26. * Affiche la page d'accueil

27. *

28. * @return View

29. */

30. public function accueil()

31. {

32. $articles = Article::select('id', 'title', 'intro_text')

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

34. ->paginate(4);

35. return $this->gen_accueil($articles);

36. }

38. /**

39. * Affiche les articles d'une catégorie

40. *

41. * @param $categorie

42. * @return View

43. */

44. public function categorie(Categorie $categorie)

45. {

46. $articles = $categorie->articles()->orderBy('created_at', 'desc')->paginate(4);

47. return View::make('accueil', array('categories' => Categorie::all(), 'articles' => $articles, 'actif' => $categorie->id));

48. }

50. /**

51. * Affiche un article

52. *

53. * @param $cat_id

54. * @param $article

55. * @return View

56. */

57. public function article($cat_id, Article $article)

58. {

59. //////////////////////////////////////////////////////////////

60. // Log d'un utilisateur pour tester la saisie des commentaires

61. $user = User::where('username', '=', 'admin')->first();

62. Auth::login($user);

63. //////////////////////////////////////////////////////////////

64. $comments = $article->comments()->orderBy('comments.created_at', 'desc')

65. ->join('users', 'users.id', '=', 'comments.user_id')

66. ->select('users.username', 'title', 'text', 'comments.created_at')

67. ->get();

68. return View::make('article', array('categories' => Categorie::all(), 'article' => $article, 'actif' => $cat_id, 'comments' => $comments));

69. }

71. /**

72. * Traitement du formulaire de recherche

73. *

74. * @return View ou Redirect

75. */

76. public function find()

77. {

78. $match = Input::get('find');

79. if($match) {

80. $articles = Article::select('id', 'title', 'intro_text')

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

82. ->where('intro_text', 'like', '%'.$match.'%')

83. ->orwhere('full_text', 'like', '%'.$match.'%')

84. ->get();

85. Session::flash('flash_notice', 'Résultats pour la recherche du terme '.$match);

86. return $this->gen_accueil($articles);

87. }

88. return Redirect::to('/')->with('flash_error', 'Il faudrait entrer un terme pour la recherche !');

89. }

91. /**

92. * Traitement du formulaire d'ajout de commentaires

93. *

94. * @return Redirect

95. */

96. public function comment()

97. {

98. Comment::create(array(

99. 'title' => Input::get('title'),

100. 'user_id' => Auth::user()->id,

101. 'article_id' => Input::get('id_art'),

102. 'text' => Input::get('comment')

103. ));

104. return Redirect::to(url('art', array(Input::get('id_cat'), Input::get('id_art'))));

105. }

107. }

Il reprend le code que nous avons déjà vu. J'ai cette fois bien séparé les fonctions, en particulier entre l'accueil et la recherche. Remarquez l'application dans le constructeur du filtre csrf pour la méthode POST. On pourrait aussi mettre ce filtre dans les routes en groupant les deux qui sont concernées.

Au niveau de l'affichage des articles, j'ai prévu l'authentification artificielle d'un utilisateur pour avoir accès à la saisie des commentaires. On supprimera ce code dans un chapitre ultérieur consacré à l'authentification.

XXI-C - Les vues

Les vues ont subi quelques améliorations que je vous livre sans commentaires.

Voici le template template.blade.php :

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

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

5. <title>

6. Mon joli blog

7. </title>

8. <meta name="viewport" content="width=device-width, initial-scale=1.0">

9. {{ HTML::style('//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css') }}

10. {{ HTML::style('//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap-theme.min.css') }}

11. {{ HTML::style('assets/css/main.css') }}

12. {{ HTML::style('http://fonts.googleapis.com/css?family=Imprima') }}

13. </head>

15. <body>

16. <div class="container">

18. <header class="jumbotron" id="entete">

19. <h1>Mon joli blog !</h1>

20. </header>

22. <nav class="navbar navbar-default">

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

24. <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">

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

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

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

28. </button>

29. <a class="navbar-brand" href="#">Mon joli blog</a>

30. </div>

31. <div class="collapse navbar-collapse">

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

33. @yield('navigation')

34. </ul>

35. {{ Form::open(array('url' => 'find', 'method' => 'POST', 'class' => 'navbar-form navbar-left pull-right')) }}

36. {{ Form::text('find', '', array('class' => 'form-group form-control', 'placeholder' => 'Recherche')) }}

37. {{ Form::close() }}

38. </div>

39. </nav>

41. <section class="row">

42. @yield('content')

43. </section>

45. <footer>

46. <em>© 2013</em>

47. </footer>

49. </div>

50. {{ HTML::script('//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js'); }}

51. {{ HTML::script('//netdna.bootstrapcdn.com/bootstrap/3.0.2/js/bootstrap.min.js'); }}

52. </body>

53. </html>

La navigation navigation.blade.php :

1. @section('navigation')

2. <li>{{ link_to_route('accueil', 'Accueil', null, ($actif == 0)? array('class' => 'actif'): null) }}</li>

4. @foreach ($categories as $categorie)

5. <li>{{ link_to('cat/'.$categorie->id, $categorie->title, ($actif == $categorie->id)? array('class' => 'actif'): null) }}</li>

6. @endforeach

7. @stop

L'accueil accueil.blade.php :

1. @extends('template_blog')

3. @include('navigation')

5. @section('content')

7. @if (Session::has('flash_notice'))

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

9. <div class="alert alert-success">

10. {{ Session::get('flash_notice') }}

11. </div>

12. </div>

13. @endif

15. @if (Session::has('flash_error'))

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

17. <div class="alert alert-danger">

18. {{ Session::get('flash_error') }}

19. </div>

20. </div>

21. @endif

23. @for ($i = 0; $i < count($articles); $i++)

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

25. <h3>{{$articles[$i]->title}}</h3>

26. <p>{{$articles[$i]->intro_text}}</p>

27. <a class="btn btn-default" href="{{ url('art/'.$actif.'/'.$articles[$i]->id) }}">Lire la suite <span class="glyphicon glyphicon-play"></span></a>

28. </div>

29. @endfor

31. @if (method_exists($articles,'links'))

32. {{$articles->links()}}

33. @endif

35. @stop

Et enfin la vue pour les articles article.blade.php :

1. @extends('template_blog')

3. @include('navigation')

5. @section('content')

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

8. <div class="well">

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

10. Ecrit le {{date('d-m-Y',strtotime($article->created_at))}}

11. </em>

12. <h3>

13. {{$article->title}}

14. </h3>

15. <p>

16. {{$article->full_text}}

17. </p>

18. </div>

19. </div>

21. @foreach ($comments as $comment)

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

23. <div class="comment">

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

25. Ecrit par {{$comment->username}} le {{date('d-m-Y',strtotime($comment->created_at))}}

26. </em>

27. <h5>

28. {{$comment->title}}

29. </h5>

30. <p>

31. {{$comment->text}}

32. </p>

33. </div>

34. </div>

35. @endforeach

37. @if ($article->allow_comment and Auth::check())

38. <hr />

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

40. <div class="well">

41. {{ Form::open(array('url' => 'comment')) }}

42. {{ Form::hidden('id_art', $article->id) }}

43. {{ Form::hidden('id_cat', $actif) }}

44. {{ Form::label('title', 'Titre de votre commentaire :') }}

45. {{ Form::text('title', '', $attributes = array('class' => 'form-control')) }}

46. {{ Form::label('comment', 'Votre commentaire :') }}

47. {{ Form::textarea('comment', '', $attributes = array('class' => 'form-control')) }}

48. {{ Form::submit('Envoyer', array('class' => 'btn btn-default')) }}

49. {{ Form::close() }}

50. </div>

51. </div>

52. @endif

54. @stop

XXI-D - Conclusion

Le code est maintenant mieux organisé avec un fichier de routes léger et lisible et un contrôleur pour assurer la gestion de l'application. Le tout est-il maintenant satisfaisant ?

Pour le moment nous allons nous en satisfaire, mais ce n'est pas une idée excellente de trouver le traitement des données dans le contrôleur. Dans l'idéal un contrôleur devrait juste recevoir des requêtes et leur apporter des réponses. Il existe un principe nommé « Repository Pattern » qui consiste à créer un intermédiaire entre le contrôleur et les modèles d'Eloquent. Nous verrons dans des chapitres ultérieurs comment effectuer ce découplage.

XXII - Chapitre 22 : Installer des générateurs

Il commence à apparaître des aides dans l'utilisation de Laravel 4. Il y en a une qui me plaît bien qui a été créée par Jeffrey Way, c'est un ensemble de générateurs qui accélèrent le codage.

XXII-A - Installation du paquetage

Vous pouvez trouver le paquetage ici :

Vous trouvez la procédure d'installation sur Github. La première chose à faire est d'ajouter la référence au fichier composer.json :

1. {

2. "require": {

3. "laravel/framework": "4.0.*"

4. "way/generators": "dev-master"

5. },

6. "autoload": {

7. "classmap": [

8. "app/commands",

9. "app/controllers",

10. "app/models",

11. "app/database/migrations",

12. "app/database/seeds",

13. "app/tests/TestCase.php"

14. ]

15. },

16. "scripts": {

17. "pre-update-cmd": [

18. "php artisan clear-compiled"

19. ],

20. "post-install-cmd": [

21. "php artisan optimize"

22. ],

23. "post-update-cmd": [

24. "php artisan optimize"

25. ]

26. },

27. "config": {

28. "preferred-install": "dist"

29. },

30. "minimum-stability": "dev"

31. }

Il serait d'ailleurs plus judicieux de le placer dans une rubrique require-dev :

"require-dev": {
"way/generators": "dev-master"
},

Lors de la mise à jour, les paquetages situés dans cette rubrique sont chargés par défaut (mettez bien à jour votre composer s'il vous le demande !). Lorsque vous voulez avoir ensuite seulement les paquetages utiles à la production vous ajoutez -no-dev à la commande update.

Faites ensuite une mise à jour avec composer :

Vous attendez que tout soit en place. Il ne reste alors plus qu'à ajouter le service provider (fourniseur) dans le fichier app/config/app.php :

1. 'Illuminate\Validation\ValidationServiceProvider',

2. 'Illuminate\View\ViewServiceProvider',

3. 'Illuminate\Workbench\WorkbenchServiceProvider',

4. 'Way\Generators\GeneratorsServiceProvider'

5. ),

Maintenant si vous utilisez la commande php artisan vous remarquez l'apparition de nouvelles commandes :

Si vous utilisez Sublime Text il existe un plugin dédié pour ces générateurs.

XXII-A-1 - Créer une migration

XXII-A-1-a - Créer une table

Voyons ce que donne la génération d'une migration avec cet outil :

J'ai demandé la création d'une migration pour une table nommée photos. Voici le résultat dans le fichier 2013_05_31_132938_create_photos_table.php (évidemment la date dépend du moment où vous lancez la commande ) :

1. use Illuminate\Database\Migrations\Migration;

2. use Illuminate\Database\Schema\Blueprint;

4. class CreatePhotoTable extends Migration {

6. /**

7. * Run the migrations.

8. *

9. * @return void

10. */

11. public function up()

12. {

13. Schema::create('photos', function(Blueprint $table) {

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

16. $table->timestamps();

17. });

18. }

20. /**

21. * Reverse the migrations.

22. *

23. * @return void

24. */

25. public function down()

26. {

27. Schema::drop('photos');

28. }

30. }

J'obtiens bien la création de la table avec déjà en place la déclaration de l'index et du timestamp. J'ai aussi la suppression de la table dans la fonction down ! On supprime ce fichier et on va plus loin en déclarant aussi des champs :

J'obtiens le même code avec en complément les deux déclarations de champ :

1. public function up()

2. {

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

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

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

6. $table->integer('categorie');

7. $table->timestamps();

8. });

9. }

XXII-A-1-b - Ajouter un champ

Allons encore plus loin, imaginons que je veux ajouter un champ style_id à ma table :

Voyons le contenu de ce nouveau fichier de migration :

1. use Illuminate\Database\Migrations\Migration;

2. use Illuminate\Database\Schema\Blueprint;

4. class AddStyleIdToPhotosTable extends Migration {

6. /**

7. * Run the migrations.

8. *

9. * @return void

10. */

11. public function up()

12. {

13. Schema::table('photos', function(Blueprint $table) {

14. $table->integer('style_id');

15. });

16. }

18. /**

19. * Reverse the migrations.

20. *

21. * @return void

22. */

23. public function down()

24. {

25. Schema::table('photos', function(Blueprint $table) {

26. $table->dropColumn('style_id');

27. });

28. }

30. }

On a bien la création du champ et aussi dans la fonction down la suppression de ce champ. Tout semble bien se passer

XXII-A-1-c - Supprimer un champ

Voyons maintenant comment supprimer un champ :

Résultat :

1. use Illuminate\Database\Schema\Blueprint;

3. class RemoveCategorieFromPhotosTable extends Migration {

5. /**

6. * Run the migrations.

7. *

8. * @return void

9. */

10. public function up()

11. {

12. Schema::table('photos', function(Blueprint $table) {

13. $table->dropColumn('categorie');

14. });

15. }

17. /**

18. * Reverse the migrations.

19. *

20. * @return void

21. */

22. public function down()

23. {

24. Schema::table('photos', function(Blueprint $table) {

25. $table->integer('categorie');

26. });

27. }

29. }

On a bien la suppression de la colonne categorie en up et sa recréation en down.

XXII-A-2 - Créer un modèle

On peut facilement créer un modèle :

On retrouve bien le fichier app/models/Photo.php :

1. class Photo extends Eloquent {

2. protected $guarded = array();

3. public static $rules = array();

4. }

Les deux propriétés ajoutées ne vous seront utiles que si vous utilisez certaines fonctionnalités des paquetages de Jeffrey Way.

XXII-A-3 - Créer des seeds

Il est aussi facile de créer des seeds, autrement dit du code pour remplir les tables :

Résultat avec le fichier app/database/seeds/PhotosTableSeeder.php :

1. class PhotosTableSeeder extends Seeder {

3. public function run()

4. {

5. // Uncomment the below to wipe the table clean before populating

6. // DB::table('photos')->delete();

8. $photos = array(

10. );

12. // Uncomment the below to run the seeder

13. // DB::table('photos')->insert($photos);

14. }

16. }

Le générateur est suffisamment gentil pour mettre à jour aussi le fichier app/database/seeds/DatabaseSeeder.php :

1. class DatabaseSeeder extends Seeder {

3. /**

4. * Run the database seeds.

5. *

6. * @return void

7. */

8. public function run()

9. {

10. Eloquent::unguard();

12. // $this->call('UserTableSeeder');

13. $this->call('PhotosTableSeeder');

14. }

16. }

Il ne vous reste plus qu'à compléter le tableau. Par exemple :

1. class PhotosTableSeeder extends Seeder {

3. public function run()

4. {

5. $photos = array(

6. array(

7. 'nom' => 'montagne',

8. 'categorie' => '1'

9. ),

10. array(

11. 'nom' => 'mer',

12. 'categorie' => '2'

13. )

14. );

16. DB::table('photos')->insert($photos);

17. }

19. }

En utilisant Artisan (ça va marcher si vous avez effectivement créé la table avec une migration auparavant) :

XXII-A-4 - Créer une vue

On peut créer une vue :

Mais là on ne crée pas grand-chose, juste le fichier app/views/photo.blade.php :

photo.blade

Évidemment le générateur ne peut pas aller plus loin .

XXII-A-5 - Créer une ressource

Ici le générateur est particulièrement puissant. Imaginez que vous voulez créer une ressource pour des livres. Voilà la syntaxe si on veut une table livres avec les champs titre et auteur :

On voit que le générateur crée pas mal de choses !

D'abord le modèle app/models/Livre.php :

1. class Livre extends Eloquent {

2. protected $guarded = array();

4. public static $rules = array();

5. }

Ensuite la migration app/database/migrations/2013_05_31_132938_create_photo_table.php :

1. use Illuminate\Database\Migrations\Migration;

2. use Illuminate\Database\Schema\Blueprint;

4. class CreateLivresTable extends Migration {

6. /**

7. * Run the migrations.

8. *

9. * @return void

10. */

11. public function up()

12. {

13. Schema::create('livres', function(Blueprint $table) {

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

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

16. $table->string('auteur');

17. $table->timestamps();

18. });

19. }

21. /**

22. * Reverse the migrations.

23. *

24. * @return void

25. */

26. public function down()

27. {

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

29. }

31. }

Ensuite quatre vues dans un dossier app/views/livres :

Et pour terminer un seed pour remplir la table app/database/seeds/LivresTableSeeder.php :

1. class LivresTableSeeder extends Seeder {

3. public function run()

4. {

5. // Uncomment the below to wipe the table clean before populating

6. // DB::table('livres')->delete();

8. $livres = array(

10. );

12. // Uncomment the below to run the seeder

13. // DB::table('livres')->insert($livres);

14. }

16. }

On trouve aussi la route :

Route::resource('livres', 'LivresController');

Et le contrôleur app/controllers/LivresController.php :

1. class LivresController extends BaseController {

3. /**

4. * Display a listing of the resource.

5. *

6. * @return Response

7. */

8. public function index()

9. {

10. //

11. }

13. /**

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

15. *

16. * @return Response

17. */

18. public function create()

19. {

20. //

21. }

23. /**

24. * Store a newly created resource in storage.

25. *

26. * @return Response

27. */

28. public function store()

29. {

30. //

31. }

33. /**

34. * Display the specified resource.

35. *

36. * @param  int  $id

37. * @return Response

38. */

39. public function show($id)

40. {

41. //

42. }

44. /**

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

46. *

47. * @param  int  $id

48. * @return Response

49. */

50. public function edit($id)

51. {

52. //

53. }

55. /**

56. * Update the specified resource in storage.

57. *

58. * @param  int  $id

59. * @return Response

60. */

61. public function update($id)

62. {

63. //

64. }

66. /**

67. * Remove the specified resource from storage.

68. *

69. * @param  int  $id

70. * @return Response

71. */

72. public function destroy($id)

73. {

74. //

75. }

77. }

XXII-A-6 - Utilisation de la ressource

On ne va pas rester en si bon chemin et on va maintenant utiliser la ressource que nous venons de créer. D'abord la migration :

Et l'insertion de quelques livres :

1. class LivresTableSeeder extends Seeder {

3. public function run()

4. {

5. $livres = array(

6. array(

7. 'titre' => 'La grande barrique',

8. 'auteur' => 'Le Gnouf'

9. ),

10. array(

11. 'titre' => 'Ma mer',

12. 'auteur' => 'Jean Veut'

13. )

14. );

16. DB::table('livres')->insert($livres);

17. }

19. }

XXII-A-6-a - Index

Voyons à présent si tout ça fonctionne. On va renseigner la méthode index du contrôleur :

1. public function index()

2. {

3. return View::make('livres.index')->with('livres', Livre::all());

4. }

Et évidemment renseigner aussi la vue app/view/livres/index.blade.php :

1. @foreach ($livres as $livre)

2. <p>Titre du livre : {{ $livre->titre }}</p>

3. <p>Auteur : {{ $livre->auteur }}</p>

4. @endforeach

Et voilà le résultat avec l'URL http://localhost/laravel/public/livres :

1. Titre du livre : La grande barrique

3. Auteur : Le Gnouf

5. Titre du livre : Ma mer

7. Auteur : Jean Veut

XXII-A-6-b - Show

Puisqu'on est bien parti, renseignons aussi la fonction show :

1. public function show($id)

2. {

3. return View::make('livres.show')->with('livre', Livre::find($id));

4. }

Et la vue app/view/livres/show.blade.php :

<p>Titre du livre : {{ $livre->titre }}</p>
<p>Auteur : {{ $livre->auteur }}</p>

Et voilà le résultat avec l'URL http://localhost/laravel/public/livres/1 :

Titre du livre : La grande barrique

Auteur : Le Gnouf

XXII-A-6-c - Create

Voyons à présent la fonction create, renseignons le contrôleur :

1. public function create()

2. {

3. return View::make('livres.create');

4. }

Et la vue app/view/livres/create.blade.php :

1. {{ Form::open(array('url' => 'livres', 'method' => 'POST')) }}

2. {{ Form::label('titre', 'Titre :')}}

3. {{ Form::text('titre') }}

4. {{ Form::label('auteur', 'Auteur :')}}

5. {{ Form::text('auteur') }}

6. {{ Form::submit('Envoyer') }}

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

Et voilà le résultat avec l'URL http://localhost/laravel/public/livres/create :

On doit utiliser la méthode POST. Au retour on tombe sur la fonction store. Pour simplifier on ne va pas prévoir de validation, le but étant juste de voir le mécanisme de la ressource REST :

1. public function store()

2. {

3. $livre = new Livre;

4. $livre->create(Input::all());

5. return 'Livre enregistré';

6. }

Faisons un essai :

XXII-A-6-d - Edit

Le résultat est satisfaisant. Voyons maintenant l'édition d'un livre avec la fonction edit :

1. public function edit($id)

2. {

3. return View::make('livres.edit')->with('livre', Livre::find($id));

4. }

Et la vue app/view/livres/edit.blade.php :

1. {{ Form::open(array('url' => 'livres/'.$livre->id, 'method' => 'PUT')) }}

2. {{ Form::label('titre', 'Titre :')}}

3. {{ Form::text('titre', $livre->titre) }}

4. {{ Form::label('auteur', 'Auteur :')}}

5. {{ Form::text('auteur', $livre->auteur) }}

6. {{ Form::submit('Envoyer') }}

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

Et voilà le résultat avec l'URL http://localhost/laravel/public/livres/1/edit :

On doit utiliser la méthode PUT. Remarquez dans le code généré que Laravel ajoute ce contrôle caché pour que la méthode PUT soit prise en compte (nos navigateurs ne connaissent que POST et GET) :

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

Au retour on tombe sur la fonction update. Pour simplifier encore, on ne va pas prévoir de validation :

1. public function update($id)

2. {

3. $livre = Livre::find($id);

4. $livre->update(Input::all());

5. return 'Livre modifié';

6. }

On va juste changer l'orthographe du nom :

XXII-A-6-e - Destroy

Tout a encore l'air de bien se passer . Il ne nous reste plus qu'à voir la suppression d'un livre avec la fonction destroy :

1. public function destroy($id)

2. {

3. $livre = Livre::find($id);

4. $livre->delete();

5. return 'Livre supprimé';

6. }

Maintenant, si on utilise l'URL http://localhost/laravel/public/livres/4 avec la méthode DELETE, on va supprimer le livre avec l'id 4. Mais la question est : comment avoir cette méthode ? On peut envisager diverses possibilités, je vous en propose une qui est simple pour tester le contrôleur en modifiant un peu la vue app/view/livres/index.blade.php :

1. @foreach ($livres as $livre)

2. <p>Titre du livre : {{ $livre->titre }}</p>

3. <p>Auteur : {{ $livre->auteur }}</p>

4. @endforeach

6. <p>Choisissez le livre à supprimer :</p>

7. @foreach ($livres as $livre)

8. {{ Form::open(array('url' => 'livres/'.$livre->id, 'method' => 'DELETE')) }}

9. {{ Form::submit($livre->titre.' de '.$livre->auteur) }}

10. {{ Form::close() }}

11. @endforeach

Vous obtenez des boutons pour supprimer les livres avec l'URL http://localhost/laravel/public/livres :

On peut évidemment utiliser AJAX pour créer des solutions plus efficaces .

XXII-A-6-f - Synthèse

Faisons une petite synthèse en modifiant encore une fois la vue app/view/livres/index.blade.php :

1. <style type="text/css">

2. form {margin: 0}

3. a {text-decoration: none}

4. table {

5. border: medium solid #6495ed;

6. border-collapse: collapse;

7. }

8. th, td {

9. border: thin solid #6495ed;

10. padding: 5px;

11. }

12. th {background-color: #D0E3FA}

13. td {background-color: #ffffff}

14. </style>

15. @if (Session::has('flash_notice'))

16. <p>{{ Session::get('flash_notice') }}</p>

17. @endif

18. <table>

19. <caption><h2>Livres en stock</h2></caption>

20. <tr>

21. <th>Titre</th>

22. <th>Auteur</th>

23. <th colspan=3><a href="{{ url('livres/create')}}"> <input type="button" value="Ajouter un livre">  </a></th>

24. </tr>

25. @foreach ($livres as $livre)

26. <tr>

27. <td>{{ $livre->titre }}</td>

28. <td>{{ $livre->auteur }}</td>

30. <td>

31. <a href="{{ url('livres/'.$livre->id)}}"> <input type="button" value="Voir">  </a>

32. </td>

33. <td>

34. <a href="{{ url('livres/'.$livre->id.'/edit')}}"> <input type="button" value="Modifier">  </a>

35. </td>

36. <td>

37. {{ Form::open(array('url' => 'livres/'.$livre->id, 'method' => 'DELETE')) }}

38. {{ Form::submit('Supprimer', array('onclick' => 'return confirm(\'Vraiment supprimer ce livre ?\')')) }}

39. {{ Form::close() }}

40. </td>

41. </tr>

42. @endforeach

43. </table>

Et voilà le résultat avec l'URL http://localhost/laravel/public/livres :

Cette fois j'ai fait un petit effort de présentation . Vous voyez qu'on peut obtenir quelque chose de fonctionnel assez rapidement avec ce générateur. Pour boucler l'application on modifie le contrôleur pour renvoyer à cette page après chaque opération :

1. class LivresController extends BaseController {

3. /**

4. * Display a listing of the resource.

5. *

6. * @return Response

7. */

8. public function index()

9. {

10. return View::make('livres.index')->with('livres', Livre::all());

11. }

13. /**

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

15. *

16. * @return Response

17. */

18. public function create()

19. {

20. return View::make('livres.create');

21. }

23. /**

24. * Store a newly created resource in storage.

25. *

26. * @return Response

27. */

28. public function store()

29. {

30. $livre = new Livre;

31. $livre->create(Input::all());

32. return Redirect::to('livres')->with('flash_notice', 'Livre bien ajouté.');

33. }

35. /**

36. * Display the specified resource.

37. *

38. * @param  int  $id

39. * @return Response

40. */

41. public function show($id)

42. {

43. return View::make('livres.show')->with('livre', Livre::find($id));

44. }

46. /**

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

48. *

49. * @param  int  $id

50. * @return Response

51. */

52. public function edit($id)

53. {

54. return View::make('livres.edit')->with('livre', Livre::find($id));

55. }

57. /**

58. * Update the specified resource in storage.

59. *

60. * @param  int  $id

61. * @return Response

62. */

63. public function update($id)

64. {

65. $livre = Livre::find($id);

66. $livre->update(Input::all());

67. return Redirect::to('livres')->with('flash_notice', 'Livre bien modifié.');

68. }

70. /**

71. * Remove the specified resource from storage.

72. *

73. * @param  int  $id

74. * @return Response

75. */

76. public function destroy($id)

77. {

78. $livre = Livre::find($id);

79. $livre->delete();

80. return Redirect::to('livres')->with('flash_notice', 'Livre bien supprimé.');

81. }

83. }

Notez qu'on peut faire la redirection en désignant aussi le contrôleur, par exemple pour la fonction update :

return Redirect::action('LivresController@index')->with('flash_notice', 'Livre bien modifié.');

XXII-A-7 - Créer un « scaffold »

Vous pouvez aller encore plus loin avec ces générateurs et utiliser la commande scaffold. C'est quoi  ? Tout simplement le générateur qui va non seulement créer tout ce qu'on a vu précédemment, mais en plus les vues seront complètes et fonctionnelles ! La syntaxe est la même que pour les ressources. Voyons un exemple :

Au niveau de la génération, très peu de différences si ce n'est un layout et un fichier de tests (on peut se demander d'ailleurs pourquoi la génération d'une ressource ne crée plus le fichier de test ). Lançons la migration pour créer la table :

Que se passe-t-il maintenant avec l'URL http://localhost/laravel/public/adresses :

Bon… ça fait partie des joies des générateurs . Apparemment quelque part notre classe Adresse a perdu son « e ». C'est apparemment un souci avec les pluriels entre la langue anglaise et la nôtre . Si vous prenez le mot pluriel anglais adresses en le mettant au singulier ça devient adress, alors que pour nous Français ça devient adresse. On trouve le bogue dans le contrôleur :

1. public function __construct(Adress $adress)

2. {

3. $this->adress = $adress;

4. }

Il suffit de corriger les trois références du nom de la classe… Et maintenant c'est mieux :

On peut maintenant créer une adresse :

Vous avez ainsi une trame de base fonctionnelle avec validations et tests .

XXII-A-8 - Générer un formulaire

Voyons maintenant la dernière commande disponible. Elle permet de créer des formulaires. Mais ça fonctionne uniquement si vous avez un modèle. Prenons le cas du modèle vu précédemment pour les livres :

La commande est bien allée chercher les deux champs pour créer le formulaire avec le bon type. On peut juste se demander si l'utilisation d'une liste est judicieuse ? Pas toujours. On a la possibilité de demander une autre mise en forme avec l'option HTML :

XXIII - Chapitre 23 : Les helpers

Cette liste des helpers avait été établie avant documentation, je la laisse en essayant de la compléter au mieux…

XXIII-A - La classe « HTML »

XXIII-A-1 - La méthode « entities »

Cette méthode utilise la méthode htmlentities avec l'option ENT_QUOTES avec l'encodage UTF-8 :

Route::get('/', function() {
return HTML::entities('L\'enfant est <strong>gros</strong>');
});

Affiche :

L'enfant est <strong>gros</strong>

Avec ce code HTML :

L&#039;enfant est &lt;strong&gt;gros&lt;/strong&gt;

Notez que l'helper « e » fait exactement la même chose avec une syntaxe plus légère.

XXIII-A-1-a - Méthode « decode »

C'est l'inverse de la précédente :

Route::get('/', function() {
return HTML::decode('L&#039;enfant est &lt;strong&gt;gros&lt;/strong&gt;');
});

Qui donne :

L'enfant est <strong>gros</strong>

Avec le code HTML :

L'enfant est <strong>gros</strong>

XXIII-A-1-b - Méthode « image »

Cette méthode permet de générer une balise <img> :

Route::get('/', function() {
return HTML::image('img/bateau.jpg');
});

Qui génère :

<img src="http://localhost/laravel/public/img/bateau.jpg">

On dispose de deux autres paramètres, le premier pour l'attribut alt, le second pour un tableau avec tous les attributs que l'on veut :

Route::get('/', function() {
return HTML::image('img/bateau.jpg', 'bateau', array('height' => 42, 'width' => 42));
});

Qui génère :

<img src="http://localhost/laravel/public/img/bateau.jpg" height="42" width="42" alt="bateau">

XXIII-A-1-c - Méthode « link »

Cette méthode permet de générer une balise <a> :

Route::get('/', function() {
return HTML::link('pages/page1.html');
});

Qui génère :

<a href="http://localhost/laravel/public/pages/page1.html">http://localhost/laravel/public/pages/page1.html</a>

On dispose de trois autres paramètres, le premier pour le titre, le second pour un tableau avec tous les attributs que l'on veut, et le troisième pour passer en mode sécurisé :

Route::get('/', function() {
return HTML::link('pages/page1.html', 'page1', array('target' => '_blank'), true );
});

Qui génère :

<a href="https://localhost/laravel/public/pages/page1.html" target="_blank">page1</a>

XXIII-A-1-d - Méthode « secureLink »

C'est la même méthode que la précédente mais le dernier paramètre n'existe plus et on est d'office en mode sécurisé.

XXIII-A-1-e - Méthode « script »

Cette méthode génère une balise script :

Route::get('/', function() {
return HTML::script('js/main.js');
});

Code généré :

<script src="http://localhost/laravel/public/js/main.js"></script>

Un deuxième paramètre permet de définir des attributs avec un tableau :

Route::get('/', function() {
return HTML::script('js/main.js', array('charset' => 'UTF-8'));
});

Résultat :

<script charset="UTF-8" src="http://localhost/laravel/public/js/main.js"></script>

XXIII-A-1-f - Méthode « style »

Cette méthode permet de générer un lien vers une feuille de style :

Route::get('/', function() {
return HTML::style('ccc/main.css');
});

Code généré :

<link media="all" type="text/css" rel="stylesheet" href="http://localhost/laravel/public/ccc/main.css">

Un deuxième paramètre permet de définir des attributs dans un tableau :

Route::get('/', function() {
return HTML::style('ccc/main.css', array('disabled' => 'true'));
});

Code généré :

<link disabled="true" media="all" type="text/css" rel="stylesheet" href="http://localhost/laravel/public/ccc/main.css">

XXIII-A-1-g - Méthode « linkRoute »

Cette méthode permet de générer un lien vers une route :

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

2. return HTML::linkRoute('log');

3. });

4. Route::get('login', array('as' => 'log', function() {

5. return View::make('hello');

6. }));

<a href="http://localhost/laravel/public/login">http://localhost/laravel/public/login</a>

On dispose de trois autres paramètres : un pour le titre, un autre pour les paramètres, et un dernier pour les attributs :

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

2. return HTML::linkRoute('log', 'log', array('nom' => 'Dupont'), array('target' => '_blank'));

3. });

4. Route::get('login', array('as' => 'log', function() {

5. return View::make('hello');

6. }));

<a href="http://localhost/laravel/public/login?nom=Dupont" target="_blank">log</a>

XXIII-A-1-h - Méthode « linkAction »

Cette méthode est calquée sur la précédente mais on pointe cette fois une action d'un contrôleur :

Route::get('/', function() {
return HTML::linkAction('LivresController@create', 'create', array('nom' => 'Dupont'), array('target' => '_blank'));
});

<a href="http://localhost/laravel/public/livres/create?nom=Dupont" target="_blank">create</a>

XXIII-A-1-i - Méthode « ol »

Cette méthode permet de générer une liste ordonnée :

Route::get('/', function() {
return HTML::ol(array('un', 'deux', 'trois'));
});

1. <ol>

2. <li>un</li>

3. <li>deux</li>

4. <li>trois</li>

5. </ol>

Vous avez un second paramètre pour les attributs :

Route::get('/', function() {
return HTML::ol(array('un', 'deux', 'trois'), array('class' => 'liste'));
});

1. <ol class="liste">

2. <li>un</li>

3. <li>deux</li>

4. <li>trois</li>

5. </ol>

XXIII-A-1-j - Méthode « ul »

Cette méthode est calquée sur la précédente :

Route::get('/', function() {
return HTML::ul(array('un', 'deux', 'trois'), array('class' => 'liste'));
});

1. <ul class="liste">

2. <li>un</li>

3. <li>deux</li>

4. <li>trois</li>

5. </ul>

XXIII-A-1-k - Les macros

Vous pouvez créer une macro quelconque :

1. HTML::macro('mon_div', function() {

2. return "<div class='contain'>";

3. });

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

5. return HTML::mon_div();

6. });

<div class='contain'>

XXIII-A-2 - Les URL

XXIII-A-2-a - Méthode « url »

La première méthode, la plus simple, est url :

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

http://localhost/laravel/public

On peut transmettre un chemin en premier paramètre :

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

http://localhost/laravel/public/test

Le deuxième paramètre sert à passer des éléments pour l'URL :

Route::get('/', function() {
return url('test', array('article', 'id'));
});

http://localhost/laravel/public/test/article/id

Le troisième paramètre sert à passer en mode sécurisé :

Route::get('/', function() {
return url('test', array(), true);
});

https://localhost/laravel/public/test

Mais vous obtenez le même résultat plus simplement avec la méthode secure_url :

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

XXIII-A-2-b - Méthode « route »

La méthode route permet de générer un URL pour une route :

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

2. return route('log');

3. });

4. Route::get('login', array('as' => 'log', function() {

5. return View::make('hello');

6. }));

http://localhost/laravel/public/login

Le deuxième paramètre permet d'ajouter des paramètres pour la route :

Route::get('/', function() {
return route('log', array('nom' => 'Dupont'));
});

http://localhost/laravel/public/login?nom=Dupont

XXIII-A-2-c - Méthode « action »

Supposons que vous avez un contrôleur nommé LivresController et une action create, entrez ce code dans les routes :

Route::get('/', function() {
return action('LivresController@create');
});

http://localhost/laravel/public/livres/create

Vous pouvez ajouter des paramètres :

Route::get('/', function() {
return action('LivresController@create', array('nom' => 'Dupont', 'prenom' => 'Albert'));
});

http://localhost/laravel/public/livres/create?nom=Dupont&prenom=Albert

XXIII-A-2-d -  Méthode « asset »

La méthode asset permet de générer un URL pour les assets (ressources), par exemple :

Route::get('/', function() {
return asset('assets/css/mon_css.css');
});

Résultat :

http://localhost/laravel/public/assets/css/mon_css.css

Un deuxième paramètre, par défaut à null, permet de passer en protocole sécurisé :

Route::get('/', function() {
return asset('assets/css/mon_css.css', true);
});

Résultat :

https://localhost/laravel/public/assets/css/mon_css.css

Vous pouvez obtenir le même résultat avec la méthode secure_asset :

Route::get('/', function() {
return secure_asset('assets/css/mon_css.css');
});

XXIII-A-3 - Les liens

XXIII-A-3-a - Méthode « link_to »

La méthode link_to permet de créer un lien :

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

<a href="http://localhost/laravel/public/test">http://localhost/laravel/public/test</a>

Vous pouvez définir le titre avec le deuxième paramètre :

Route::get('/', function() {
return link_to('test', 'titre');
});

<a href="http://localhost/laravel/public/test">titre</a>

Le troisième paramètre permet d'ajouter des attributs :

Route::get('/', function() {
return link_to('test', 'titre', array('class' => 'deco'));
});

<a href="http://localhost/laravel/public/test" class="deco">titre</a>

Le quatrième paramètre (normalement à null) permet de passer en mode sécurisé :

Route::get('/', function() {
return link_to('test', 'titre', array('class' => 'deco'), true);
});

<a href="https://localhost/laravel/public/test" class="deco">titre</a>

XXIII-A-3-b - Méthode « link_to_route »

La méthode link_to_route permet de créer un lien vers une route nommée :

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

2. return link_to_route('log');

3. });

4. Route::get('login', array('as' => 'log', function() {

5. return View::make('hello');

6. }));

<a href="http://localhost/laravel/public/login">http://localhost/laravel/public/login</a>

Vous pouvez définir le titre avec le deuxième paramètre :

Route::get('/', function() {
return link_to_route('log', 'titre');
});

<a href=">http://localhost/laravel/public/login">titre</a>

Le troisième paramètre permet d'ajouter des attributs :

Route::get('/', function() {
return link_to_route('log', 'titre', array('class' => 'deco'));
});

<a href=">http://localhost/laravel/public/login" class="deco">titre</a>

Le quatrième paramètre (normalement à null) permet de passer en mode sécurisé :

Route::get('/', function() {
return link_to_route('log', 'titre', array('class' => 'deco'), true);
});

<a href=">https://localhost/laravel/public/login" class="deco">titre</a>

XXIII-A-3-c - Méthode « link_to_action »

La méthode link_to_action permet de créer un lien vers une action d'un contrôleur :

Route::get('/', function() {
return link_to_action('LivresController@create');
});

<a href="http://localhost/laravel/public/livres/create">http://localhost/laravel/public/livres/create</a>

Vous pouvez définir le titre avec le deuxième paramètre :

Route::get('/', function() {
return link_to_action('LivresController@create', titre);
});

<a href="http://localhost/laravel/public/livres/create">titre</a>

Le troisième paramètre permet d'ajouter des paramètres :

Route::get('/', function() {
return link_to_action('LivresController@create', 'titre', array('nom' => 'Dupont'));
});

<a href="http://localhost/laravel/public/livres/create?nom=Dupont">titre</a>

Le quatrième paramètre permet de passer des attributs :

Route::get('/', function() {
return link_to_action('LivresController@create', 'titre', array(), array('class' => 'deco'));
});

<a href="http://localhost/laravel/public/livres/create" class="deco">titre</a>

XXIII-A-4 - Les chaînes de caractères

XXIII-A-4-a - Méthode « starts_with »

On peut savoir si une chaîne commence par une autre chaîne en utilisant la méthode starts_with :

Route::get('/', function() {
return var_dump(starts_with('autoroute', 'au'));
});

bool(true)

Le paramètre peut être un tableau :

Route::get('/', function() {
return var_dump(starts_with('autoroute', array('au', 'uy')));
});

Dans ce cas il suffit que l'un des éléments corresponde pour obtenir true.

XXIII-A-4-b - Méthode « ends_with »

Cette méthode est équivalente à la précédente, mais cette fois on teste la présence d'une chaîne à la fin d'une autre :

Route::get('/', function() {
return var_dump(ends_with('autoroute', 'ute'));
});

Il est aussi possible de passer un tableau comme paramètre.

XXIII-A-4-c - Méthode « str_contains »

Avec cette méthode on peut savoir si une chaîne en contient une autre, quel que soit l'emplacement (début, milieu ou fin) :

Route::get('/', function() {
return var_dump(str_contains('autoroute', 'tor'));
});

bool(true)

Il est aussi possible de passer un tableau comme paramètre.

XXIII-A-4-d - Méthode « str_is »

Cette méthode nous précise si une chaîne correspond à un certain masque :

Route::get('/', function() {
return var_dump(str_is('assets/*', 'assets/css/main.css'));
});

bool(true)

XXIII-A-4-e - Méthode « str_random »

Cette méthode génère une chaîne aléatoire, par défaut elle comporte seize caractères :

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

XBe1bd4PwFCGEq4i

On peut ajouter un deuxième paramètre pour définir le nombre de caractères :

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

arKEVOLXkC

Cette méthode ne doit pas être utilisée pour le chiffrement !

XXIII-A-4-f - Méthode « snake_case »

Cette méthode permet de passer une chaîne en snake_case :

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

ma_classe

Remarquez que vous pouvez aussi changer le séparateur :

Route::get('/', function() {
return snake_case('maClasse', '-');
});

ma-classe

XXIII-A-4-g - Méthode « camel_case »

C'est la méthode inverse de la précédente :

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

maClasse

XXIII-A-4-h - Méthode « e »

Cette méthode utilise la méthode htmlentities avec l'option ENT_QUOTES avec l'encodage UTF-8 :

Route::get('/', function() {
return e('L\'enfant est <strong>gros</strong>');
});

Elle affiche :

L'enfant est <strong>gros</strong>

Avec ce code HTML :

L&#039;enfant est &lt;strong&gt;gros&lt;/strong&gt;

XXIII-A-4-i - Méthode slug

Cette méthode transforme une phrase en chaîne utilisable pour un URL :

Route::get('/', function() {
return Str::slug("Ma phrase pas évidente");
});

ma-phrase-pas-evidente

XXIII-A-4-j - Méthode title

Cette méthode met des majuscules aux mots :

Route::get('/', function() {
return Str::title("Ma phrase pas évidente");
});

Ma Phrase Pas Évidente

XXIII-A-4-k - Méthode finish

Cette méthode ajoute une fin chaîne :

Route::get('/', function() {
return str_finish("Ma phrase pas évidente", '.');
});

Ma phrase pas évidente.

XXIII-A-4-l - Méthode limit

Cette méthode limite la longueur d'une chaîne :

Route::get('/', function() {
return str_limit("Ma phrase pas évidente", 10);
});

Ma phrase...

On peut choisir le style de fin ajouté avec un troisième paramètre :

Route::get('/', function() {
return str_limit("Ma phrase pas évidente", 10, ' !!!');
});

Ma phrase !!!

XXIII-A-4-m - Méthode words

Cette méthode limite le nombre de mots d'une chaîne :

Route::get('/', function() {
return Str::words("Ma phrase pas évidente", 3);
});

Ma phrase pas...

On peut choisir le style de fin ajouté avec un troisième paramètre :

Route::get('/', function() {
return Str::words("Ma phrase pas évidente", 3, ' !!!');
});

Ma phrase pas !!!

XXIII-A-5 - Les tableaux

XXIII-A-5-a - Méthode « array_divide »

Cette méthode permet de séparer un tableau en deux tableaux : l'un avec les clés et l'autre avec les valeurs :

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

2. $tableau = array(

3. 'un' => 1,

4. 'deux' => 2,

5. 'trois' => 3

6. );

7. return var_dump(array_divide($tableau));

8. });

1. array(2) {

2. [0]=> array(3) {

3. [0]=> string(2) "un"

4. [1]=> string(4) "deux"

5. [2]=> string(5) "trois"

6. }

7. [1]=> array(3) {

8. [0]=> int(1)

9. [1]=> int(2)

10. [2]=> int(3)

11. }

12. }

XXIII-A-5-b - Méthode « head »

Cette méthode retourne le premier élément d'un tableau :

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

2. $tableau = array(1, 2, 3);

3. dd(head($tableau));

4. });

int(1)

En fait c'est la méthode reset de PHP qui est utilisée.

XXIII-A-5-c - Méthode « last »

Cette méthode retourne le dernier élément d'un tableau :

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

2. $tableau = array(1, 2, 3);

3. dd(last($tableau));

4. });

int(3)

XXIII-A-5-d - Méthode « array_dot »

Cette méthode aplatit un tableau multidimensionnel en concaténant les clés en utilisant un point :

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

2. $tableau[0][0] = "0";

3. $tableau[0][1] = "1";

4. $tableau[1][0] = "a";

5. $tableau[1][1] = "b";

6. return var_dump(array_dot($tableau));

7. });

1. array(4) {

2. ["0.0"]=> string(1) "0"

3. ["0.1"]=> string(1) "1"

4. ["1.0"]=> string(1) "a"

5. ["1.1"]=> string(1) "b"

6. }

XXIII-A-5-e - Méthode « array_fetch »

Cette méthode fait l'inverse de la précédente :

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

2. $tableau['0.0'] = "0";

3. $tableau['0.1'] = "1";

4. $tableau['1.0'] = "a";

5. $tableau['1.1'] = "b";

6. return var_dump(array_fetch($tableau, '0'));

7. });

1. array(4) {

2. [0]=> string(1) "0"

3. [1]=> string(1) "1"

4. [2]=> string(1) "a"

5. [3]=> string(1) "b"

6. }

XXIII-A-5-f - Méthode « array_flatten »

Cette méthode aplatit un tableau multidimensionnel :

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

2. $tableau[0][0] = "0";

3. $tableau[0][1] = "1";

4. $tableau[1][0] = "a";

5. $tableau[1][1] = "b";

6. return var_dump(array_flatten($tableau));

7. });

1. array(4) {

2. [0]=> string(1) "0"

3. [1]=> string(1) "1"

4. [2]=> string(1) "a"

5. [3]=> string(1) "b"

6. }

XXIII-A-5-g - Méthode « array_first »

Cette méthode permet de trouver le premier élément d'un tableau, qui correspond à un test passé par une fonction de rappel :

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

2. $ma_fonction = function ($key, $value) { return $value > 0; };

3. $tableau = array(0, 1, 2);

4. return var_dump(array_first($tableau, $ma_fonction));

5. });

int(1)

XXIII-A-5-h - Méthode « array_only »

Cette méthode extrait des éléments d'un tableau à partir d'un tableau de clés et retourne un tableau :

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

2. $tableau = array(

3. 'un' => 1,

4. 'deux' => 2,

5. 'trois' => 3,

6. 'quatre' => 4

7. );

8. return var_dump(array_only($tableau, array('deux', 'quatre')));

9. });

1. array(2) {

2. ["deux"]=> int(2)

3. ["quatre"]=> int(4)

4. }

XXIII-A-5-i - Méthode « array_except »

Cette méthode extrait des éléments d'un tableau à partir d'un tableau de clés et retourne un tableau en excluant les éléments dont les clés sont transmises :

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

2. $tableau = array(

3. 'un' => 1,

4. 'deux' => 2,

5. 'trois' => 3,

6. 'quatre' => 4

7. );

8. return var_dump(array_except($tableau, array('deux', 'quatre')));

9. });

1. array(2) {

2. ["un"]=> int(1)

3. ["trois"]=> int(3)

4. }

XXIII-A-5-j - Méthode « array_get »

Cette méthode récupère un élément d'un tableau en utilisant la syntaxe avec un point séparateur :

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

2. $tableau[0][0] = "0";

3. $tableau[0][1] = "1";

4. $tableau[1][0] = "a";

5. $tableau[1][1] = "b";

6. return var_dump(array_get($tableau, '1.0'));

7. });

string(1) "a"

XXIII-A-5-k - Méthode « array_set »

Cette méthode affecte une valeur à un élément d'un tableau en utilisant la syntaxe avec un point séparateur :

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

2. $tableau[0][0] = "0";

3. $tableau[0][1] = "1";

4. $tableau[1][0] = "a";

5. $tableau[1][1] = "b";

6. array_set($tableau, '1.0', "d");

7. dd($tableau);

8. });

1. array(2) {

2. [0]=> array(2) {

3. [0]=> string(1) "0"

4. [1]=> string(1) "1"

5. }

6. [1]=> array(2) {

7. [0]=> string(1) "d"

8. [1]=> string(1) "b" }

9. }

XXIII-A-5-l - Méthode « array_forget »

Cette méthode supprime un élément d'un tableau en utilisant la syntaxe avec un point séparateur :

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

2. $tableau[0][0] = "0";

3. $tableau[0][1] = "1";

4. $tableau[1][0] = "a";

5. $tableau[1][1] = "b";

6. array_forget($tableau, '1.0');

7. return var_dump($tableau);

8. });

1. array(2) {

2. [0]=> array(2) {

3. [0]=> string(1) "0"

4. [1]=> string(1) "1"

5. }

6. [1]=> array(1) {

7. [1]=> string(1) "b"

8. }

9. }

XXIII-A-5-m -  Méthode « array_pull »

Cette méthode supprime un élément d'un tableau en utilisant la syntaxe avec un point séparateur et renvoie cette valeur :

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

2. $tableau[0][0] = "0";

3. $tableau[0][1] = "1";

4. $tableau[1][0] = "a";

5. $tableau[1][1] = "b";

6. return array_pull($tableau, '1.0');

7. });

Au retour on a la valeur retirée 'a'.

XXIII-A-5-n - Méthode « array_pluck »

Cette méthode crée un tableau avec les valeurs qui ont la même clé, et qui ont été récupérées dans des tableaux emboîtés (bon c'est plus simple quand on regarde l'exemple ) :

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

2. $tableau[0][0] = "un";

3. $tableau[1][0] = "deux";

4. dd(array_pluck($tableau, 0));

5. });

1. array(2) {

2. [0]=> string(2) "un"

3. [1]=> string(4) "deux"

4. }

XXIV - Chapitre 24 : Un blog : authentification

Nous allons dans ce chapitre poursuivre l'exemple du blog en ajoutant l'authentification. Je tiendrai compte des changements dus à la sortie de la version 4.1.26 de Laravel qui a changé certains éléments. Comme ce chapitre comporte beaucoup de code je l'ai rassemblé (ainsi que tous les éléments des chapitres précédents) dans un fichier compressé téléchargeable.

XXIV-A - Contrôleur HomeController

Pour les besoins du fonctionnement du blog, on avait prévu une authentification automatique dans le contrôleur HomeController au niveau de la méthode article. On va donc supprimer cette disposition maintenant qu'on va mettre en place l'authentification :

1. <?php

2. public function article($cat_id, Article $article)

3. {

4. $comments = $article->comments()->orderBy('comments.created_at', 'desc')

5. ->join('users', 'users.id', '=', 'comments.user_id')

6. ->select('users.username', 'title', 'text', 'comments.created_at')

7. ->get();

8. return View::make('article', array('categories' => Categorie::all(), 'article' => $article, 'actif' => $cat_id, 'comments' => $comments));

9. }

XXIV-B - Vues

XXIV-B-1 - La navigation

On va conserver les mêmes vues. On va juste ajouter un bouton au niveau de la navigation pour la connexion ou la déconnexion :

1. @section('navigation')

2. <li>{{ link_to_route('accueil', 'Accueil', null, ($actif == 0)? array('class' => 'actif'): null) }}</li>

4. @foreach ($categories as $categorie)

5. <li>{{ link_to('cat/'.$categorie->id, $categorie->title, ($actif == $categorie->id)? array('class' => 'actif'): null) }}</li>

6. @endforeach

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

9. <li>{{ link_to('auth/logout', 'Deconnexion') }}</li>

10. @else

11. <li>{{ link_to('auth/login', 'Connexion', ($actif == -1)? array('class' => 'actif'): null) }}</li>

12. @endif

13. @stop

La méthode check de la classe Auth permet de savoir si l'utilisateur est connecté. On peut ainsi adapter le menu en conséquence. On fait référence à des routes qui n'existent pas encore.

Quand je parle de la classe Auth ce n'est pas vraiment exact puisqu'il s'agit d'une façade qui renvoie à la classe Illuminate\Auth\AuthManager. Je parlerai des façades dans un chapitre ultérieur. Vous pouvez trouver la correspondance entre les façades et les classes dans cette partie de la documentation.

XXIV-B-2 - Le formulaire de connexion

Nous allons avoir besoin de vues pour l'authentification, on va toutes les loger dans un dossier views/auth. Nous allons commencer par la vue views/auth/login.blade.php. Cette vue sera chargée d'afficher le formulaire pour la connexion :

1. @extends('template_blog')

3. @include('navigation')

5. @section('content')

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

8. Pour vous connecter au site veuillez entrer votre pseudo et votre mot de passe dans le formulaire ci-dessous :

9. </div>

10. <br>

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

12. @if (Session::has('flash_error'))

13. <div class="col-md-7">

14. <div class="alert alert-danger">

15. {{ Session::get('flash_error') }}

16. </div>

17. </div>

18. @endif

19. <div class="col-md-7">

20. {{ Form::open(array('url' => 'auth/login', 'method' => 'POST', 'class' => 'form-horizontal well')) }}

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

22. {{ Form::label('username', 'Pseudo :', array('class' => 'col-md-3 control-label')) }}

23. <div class="col-md-9">

24. {{ Form::text('username', '', $attributes = array('class' => 'form-control')) }}

25. </div>

26. </div>

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

28. {{ Form::label('password', 'Mot de passe :', array('class' => 'col-md-3 control-label')) }}

29. <div class="col-md-9">

30. {{ Form::password('password', $attributes = array('class' => 'form-control')) }}

31. </div>

32. </div>

33. <div class="checkbox pull-right">

34. {{ Form::checkbox('souvenir') }}Se rappeler de moi

35. </div>

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

37. <div class="col-md-offset-3 col-md-9">

38. {{ Form::submit('Envoyer', array('class' => 'btn btn-success')) }}

39. </div>

40. </div>

41. {{ Form::close()}}

42. </div>

43. <div class="col-md-5">

44. <p>

45. {{ link_to('remind/remind', 'J\'ai oublié mon mot de passe...', array('class' => 'btn btn-success')) }}

46. </p>

47. </div>

48. </div>

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

50. Si vous n'avez pas encore de compte vous pouvez en créer un en cliquant sur ce bouton :

51. {{ link_to('auth/inscription', 'Je m\'inscris !', array('class' => 'btn btn-info')) }}

52. </div>

54. @stop

XXIV-B-3 - La langue

Pour avoir les messages en français il faut aller chercher le dossier fr ici. Il faut placer ce dossier dans celui des langues :

Et ensuite informer le fichier app/config/app.php :

<?php
'locale' => 'fr',

XXIV-C - Les routes

Nous allons nous occuper des routes en ajoutant cette ligne :

<?php
Route::controller('auth', 'AuthController');

On prévoit ici un contrôleur RESTful avec la méthode controller. Il s'agit d'un contrôleur un peu particulier. Le nom de ses méthodes doivent répondre à une certaine syntaxe : elles doivent débuter par le verbe de la requête (get, post, put…) et se terminer par un nom. Prenons un exemple avec la méthode getLogin. Cette méthode est composée du verbe get et du nom Login. Comme on a nommé le contrôleur auth, la route correspondante sera auth/login avec le verbe get. On se rend compte qu'avec une simple ligne de code on peut avoir autant de routes que l'on veut en créant des méthodes dans le contrôleur.

Il est possible de nommer les routes pour un contrôleur RESTful, par exemple on pourrait écrire :

<?php
Route::controller('auth', 'AuthController', ['getLogin' => 'login']);

XXIV-D - Le contrôleur AuthController

Maintenant que nous avons déclaré la route, passons au contrôleur :

1. <?php

3. class AuthController extends BaseController {

5. public function __construct()

6. {

7. $this->beforeFilter('auth', array('only' => 'getLogout'));

8. $this->beforeFilter('guest', array('except' => 'getLogout'));

9. $this->beforeFilter('csrf', array('on' => 'post'));

10. }

12. /**

13. * Affiche le formulaire de login

14. *

15. * @return View

16. */

17. protected function createView($name)

18. {

19. return View::make($name, array('categories' => Categorie::all(), 'actif' => -1));

20. }

22. /**

23. * Affiche le formulaire de login

24. *

25. * @return View

26. */

27. public function getLogin()

28. {

29. return $this->createView('auth.login');

30. }

32. /**

33. * Affiche le formulaire d'inscription

34. *

35. * @return View

36. */

37. public function getInscription()

38. {

39. return $this->createView('auth.inscription');

40. }

42. /**

43. * Traitement du formulaire de login

44. *

45. * @return Redirect

46. */

47. public function postLogin()

48. {

49. $user = array('username' => Input::get('username'), 'password' => Input::get('password'));

50. if (Auth::attempt($user, Input::get('souvenir'))) {

51. return Redirect::intended('/')

52. ->with('flash_notice', 'Vous avez été correctement connecté avec le pseudo ' . Auth::user()->username);

53. }

54. return Redirect::to('guest/login')->with('flash_error', 'Pseudo ou mot de passe non correct !')->withInput();

55. }

57. /**

58. * Traitement du formulaire d'inscription

59. *

60. * @return Redirect

61. */

62. public function postInscription()

63. {

64. $v = User::validate(Input::all());

65. if ($v->passes()) {

66. $user = new User;

67. $user->username = Input::get('username');

68. $user->email = Input::get('email');

69. $user->password = Hash::make(Input::get('password'));

70. $user->save();

71. return Redirect::route('accueil')->with('flash_notice', 'Votre compte a été créé.');

72. }

73. return Redirect::to('guest/inscription')->withErrors($v)->withInput();

74. }

76. /**

77. * Effectue le logout

78. *

79. * @return Redirect

80. */

81. public function getLogout()

82. {

83. Auth::logout();

84. return Redirect::route('accueil')->with('flash_notice', 'Vous avez été correctement déconnecté.');

85. }

87. }

Nous allons expliciter peu à peu ce code.

XXIV-E - La connexion

Lorsqu'on clique sur l'option « Connexion » du menu, on arrive sur ce formulaire en passant par la méthode getLogin du contrôleur :

Le traitement du formulaire est assuré par la méthode postLogin du contrôleur AuthController. Comme on a un contrôleur RESTful ça correspond donc à la route auth/login avec le verbe post. Voici cette méthode :

1. public function postLogin()

2. {

3. $user = array('username' => Input::get('username'), 'password' => Input::get('password'));

4. if (Auth::attempt($user, Input::get('souvenir'))) {

5. return Redirect::intended('/')

6. ->with('flash_notice', 'Vous avez été correctement connecté avec le pseudo ' . Auth::user()->username);

7. }

8. return Redirect::to('guest/login')->with('flash_error', 'Pseudo ou mot de passe non correct !')->withInput();

9. }

La méthode attempt de la classe Auth assure, d'une part la vérification des données entrées, et d'autre part que tout est correct au niveau de la connexion. On a donc deux situations à considérer : réussite ou échec.

XXIV-E-1 - Échec de la connexion

Si on entre des valeurs incorrectes on renvoie le formulaire avec un message d'erreur :

XXIV-E-2 - Réussite de la connexion

Normalement vous devez avoir dans votre table des utilisateurs l'un d'entre eux qui est nommé « admin » avec le passe « admin ». Vous devriez donc pouvoir vous connecter avec ces valeurs. Il y a deux possibilités de redirection selon la requête qui a amené sur la page de connexion. La méthode intended de la classe Redirect tient compte du fait qu'un URL a pu être mémorisée et redirigée parce que l'utilisateur n'était pas connecté. Si vous regardez le filtre auth vous avez ce code :

1. Route::filter('auth', function()

2. {

3. if (Auth::guest()) return Redirect::guest('login');

4. });

La redirection ici s'effectue avec la méthode guest. L'URL est mis en mémoire pour la méthode intended. S'il n'y a rien en mémoire alors on prend en compte l'URL passé en paramètre, dans notre cas l'URL de base du site. J'ai choisi de renvoyer sur la page d'accueil avec un message pour signaler que la connexion a bien eu lieu :

Remarquez aussi qu'on a la possibilité de mémoriser cette connexion dans un cookie si le deuxième paramètre de la méthode attempt a la valeur true. il suffit d'envoyer la valeur de la case à cocher prévue dans le formulaire pour affecter cette valeur.

XXIV-F - La déconnexion

Quand un utilisateur est connecté on fait apparaître un item « Deconnexion » au niveau du menu :

C'est la méthode getLogout du contrôleur AuthController qui assure le traitement :

1. public function getLogout()

2. {

3. Auth::logout();

4. return Redirect::route('accueil')->with('flash_notice', 'Vous avez été correctement déconnecté.');

5. }

On fait appel à la méthode logout de la classe Auth. On redirige ensuite sur la page d'accueil avec un message :

XXIV-G - Inscription d'un utilisateur

Si on clique sur le bouton « Je m'inscris ! » prévu dans la page de connexion on peut s'inscrire sur le site. La méthode getInscription du contrôleur AuthController provoque la génération du formulaire à partir de la vue.

XXIV-G-1 - La vue

Voici la vue auth/inscription.blade.php :

1. @extends('template_blog')

3. @include('navigation')

5. @section('content')

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

7. Pour vous inscrire veuillez remplir ce formulaire :

8. </div>

9. <br>

10. <div class="row col-md-12">

11. @if ($errors->count() > 0)

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

13. <div class="alert alert-danger">

14. @foreach($errors->all() as $message)

15. {{ $message }}<br />

16. @endforeach

17. </div>

18. </div>

19. @endif

20. <div class="col-md-9">

21. {{ Form::open(array('url' => 'auth/inscription', 'method' => 'POST', 'class' => 'form-horizontal well')) }}

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

23. {{ Form::label('username', 'Pseudo :', array('class' => 'col-md-4 control-label')) }}

24. <div class="col-md-8">

25. {{ Form::text('username', '', $attributes = array('class' => 'form-control')) }}

26. </div>

27. </div>

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

29. {{ Form::label('email', 'Mail :', array('class' => 'col-md-4 control-label')) }}

30. <div class="col-md-8">

31. {{ Form::text('email', '', $attributes = array('class' => 'form-control')) }}

32. </div>

33. </div>

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

35. {{ Form::label('password', 'Mot de passe :', array('class' => 'col-md-4 control-label')) }}

36. <div class="col-md-8">

37. {{ Form::password('password', $attributes = array('class' => 'form-control')) }}

38. </div>

39. </div>

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

41. {{ Form::label('password_confirmation', 'Confirmation passe :', array('class' => 'col-md-4 control-label')) }}

42. <div class="col-md-8">

43. {{ Form::password('password_confirmation', $attributes = array('class' => 'form-control')) }}

44. </div>

45. </div>

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

47. <div class="col-md-offset-4 col-md-8">

48. {{ Form::submit('Envoyer', array('class' => 'btn btn-success')) }}

49. </div>

50. </div>

51. {{ Form::close()}}

52. </div>

53. </div>

54. @stop

Avec cet aspect :

XXIV-G-2 - La validation

On va prévoir la méthode pour la validation dans le modèle User, de même que des messages personnalisés :

1. <?php

2. public static function validate($input)

3. {

4. $rules = array(

5. 'username' => 'required|Alpha|between:6,64|unique:users',

6. 'email' => 'required|email|unique:users',

7. 'password' => 'required|AlphaNum|between:6,20|confirmed'

8. );

9. $messages = array(

10. 'username.required' => 'Nous avons besoin de votre pseudo.',

11. 'username.Alpha' => 'Le pseudo doit être composé de caractères alphabétiques.',

12. 'username.between' => 'Le nombre de caractères du pseudo doit être compris entre :min et :max.',

13. 'username.unique' => 'Ce pseudo est déjà utilisé.',

14. 'email.required' => 'Nous avons besoin de votre adresse mail.',

15. 'email.email' => 'Le format de l\'adresse mail n\'est pas correct.',

16. 'email.unique' => 'Cette adresse mail est déjà utilisée.',

17. 'password.required' => 'Nous avons besoin d\'un mot de passe.',

18. 'password.Alphanum' => 'Le pseudo doit être composé de caractères alphanumériques.',

19. 'password.between' => 'Le nombre de caractères du mot de passe doit être compris entre :min et :max.',

20. 'password.confirmed' => 'La confirmation du mot de passe n\'est pas correcte.'

21. );

22. return Validator::make($input, $rules, $messages);

23. }

La localisation de la validation a déjà alimenté les discussions et continuera à le faire. Il y a plusieurs possibilités, nous en verrons d'autres dans les chapitres ultérieurs.

XXIV-G-3 - Le traitement

Le traitement du formulaire est assuré par la méthode postInscription du contrôleur AuthController :

1. <?php

2. public function postInscription()

3. {

4. $v = User::validate(Input::all());

5. if ($v->passes()) {

6. $user = new User;

7. $user->username = Input::get('username');

8. $user->email = Input::get('email');

9. $user->password = Hash::make(Input::get('password'));

10. $user->save();

11. return Redirect::route('accueil')->with('flash_notice', 'Votre compte a été créé.');

12. }

13. return Redirect::to('auth/inscription')->withErrors($v)->withInput();

14. }

En cas d'erreur de saisie, on renvoie le formulaire avec un message regroupant les problèmes rencontrés :

Si tout est correct, on renvoie sur la page d'accueil avec un message :

XXIV-H - La réinitialisation du mot de passe

Vous devez normalement déjà avoir dans la base la table password_reminders que nous avons créée plus haut.

XXIV-H-1 - Le contrôleur et la route

On peut créer le contrôleur pour la réinitialisation du mot de passe avec cette commande d'Artisan :

php artisan auth:reminders-controller

Ce qui a pour effet de créer un contrôleur nommé RemindersController. On va juste le modifier pour l'adapter à notre application :

1. <?php

3. class RemindersController extends Controller {

5. /**

6. * Affiche le formulaire d'oubli

7. *

8. * @return Response

9. */

10. public function getRemind()

11. {

12. return View::make('auth.remind', array('categories' => Categorie::all(), 'actif' => -1));

13. }

15. /**

16. * Traitement du formulaire d'oubli

17. *

18. * @return Response

19. */

20. public function postRemind()

21. {

22. $response = Password::remind(Input::only('email'), function($message)

23. {

24. $message->subject('Oubli du mot de passe.');

25. });

26. switch ($response)

27. {

28. case Password::INVALID_USER:

29. return Redirect::back()->with('error', Lang::get($response))->withInput();

31. case Password::REMINDER_SENT:

32. return Redirect::back()->with('status', Lang::get($response))->withInput();

33. }

34. }

36. /**

37. * Display the password reset view for the given token.

38. *

39. * @param  string  $token

40. * @return Response

41. */

42. public function getReset($token = null)

43. {

44. if (is_null($token)) App::abort(404);

46. return View::make('auth.reset', array('categories' => Categorie::all(), 'actif' => -1, 'token' => $token));

47. }

49. /**

50. * Handle a POST request to reset a user's password.

51. *

52. * @return Response

53. */

54. public function postReset()

55. {

56. $credentials = Input::only(

57. 'email', 'password', 'password_confirmation', 'token'

58. );

60. $response = Password::reset($credentials, function($user, $password)

61. {

62. $user->password = Hash::make($password);

64. $user->save();

65. });

67. switch ($response)

68. {

69. case Password::INVALID_PASSWORD:

70. case Password::INVALID_TOKEN:

71. case Password::INVALID_USER:

72. return Redirect::back()->with('error', Lang::get($response))->withInput();

74. case Password::PASSWORD_RESET:

75. return Redirect::to('auth/login');

76. }

77. }

79. }

Comme il s'agit d'un contrôleur RESTful on va prévoir une route adaptée :

<?php
Route::controller('remind', 'RemindersController');

XXIV-H-2 - Le formulaire de demande de réinitialisation

Lorsqu'on clique sur le bouton « J'ai oublié mon mot de passe… » placé sur la page de connexion, on active la méthode getRemind du contrôleur RemindersController :

1. <?php

2. public function getRemind()

3. {

4. return View::make('auth.remind', array('categories' => Categorie::all(), 'actif' => -1));

5. }

Ce qui a pour effet d'activer la vue auth/remind.blade.php :

1. @extends('template_blog')

3. @include('navigation')

5. @section('content')

7. <br>

8. <div class="row col-md-12">

9. <h3 class="col-md-12">

10. Nouveau mot de passe

11. </h3>

12. @if (!Session::has('flash_notice'))

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

14. Il semblerait que vous ayez oublié votre mot de passe. Ne vous affolez pas, nous allons vous donner la possibilité d'en créer un nouveau. Veuillez entrer dans ce formulaire l'adresse mail que vous avez utilisée pour votre inscription :

15. </div>

16. @endif

17. <br />

18. @if (Session::has('error'))

19. <div class="col-md-8">

20. <div class="alert alert-danger">

21. {{ Session::get('error') }}

22. </div>

23. </div>

24. @endif

25. @if (Session::has('status'))

26. <div class="col-md-8">

27. <div class="alert alert-success">

28. {{ Session::get('status') }}

29. </div>

30. </div>

31. @else

32. <div class="col-md-8">

33. {{ Form::open(array('url' => 'remind/remind', 'method' => 'POST', 'class' => 'form-horizontal well')) }}

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

35. {{ Form::label('email', 'Mail :', array('class' => 'col-md-2 control-label')) }}

36. <div class="col-md-10">

37. {{ Form::text('email', '', $attributes = array('class' => 'form-control')) }}

38. </div>

39. </div>

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

41. <div class="col-md-offset-2 col-md-10">

42. {{ Form::submit('Envoyer', array('class' => 'btn btn-success')) }}

43. </div>

44. </div>

45. {{ Form::close()}}

46. </div>

47. @endif

48. </div>

49. @stop

Et la création de ce formulaire :

XXIV-H-3 - Le traitement de la demande

Le traitement du formulaire est assuré par la méthode postRemind du contrôleur RemindersController :

1. <?php

2. public function postRemind()

3. {

4. $response = Password::remind(Input::only('email'), function($message)

5. {

6. $message->subject('Oubli du mot de passe.');

7. });

8. switch ($response)

9. {

10. case Password::INVALID_USER:

11. return Redirect::back()->with('error', Lang::get($response))->withInput();

13. case Password::REMINDER_SENT:

14. return Redirect::back()->with('status', Lang::get($response))->withInput();

15. }

16. }

Si l'utilisateur n'est pas trouvé, donc si l'email n'est pas valide ou n'existe pas dans la table, on renvoie le formulaire avec mention de l'erreur :

Si l'utilisateur est trouvé on lui présente un message de confirmation :

On mémorise un jeton d'identification dans la table password_reminders :

Et un email est envoyé, constitué avec la vue emails/auth/reminder.blade.php :

1. <!DOCTYPE html>

2. <html lang="fr-FR">

3. <head>

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

5. </head>

6. <body>

7. <h2>Renouvellement de mot de passe</h2>

9. <div>

10. Pour renouveler votre mot de passe veuillez utiliser ce lien :

11. <p>

12. {{ url('remind/reset/'.$token) }}

13. </p>

14. </div>

15. </body>

16. </html>

J'ai adapté le code pour notre exemple et je l'ai francisé.

Lorsque l'utilisateur clique sur le lien, il est dirigé sur la méthode getReset qui affiche le formulaire de saisie du nouveau mot de passe avec la vue auth/reset.blade.php :

1. @extends('template_blog')

3. @include('navigation')

5. @section('content')

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

7. Pour entrer un nouveau mot de passe veuillez remplir ce formulaire :

8. </div>

9. <br>

10. <div class="row col-md-12">

11. @if (Session::has('error'))

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

13. <div class="alert alert-danger">

14. {{ Session::get('error') }}

15. </div>

16. </div>

17. @endif

18. <div class="col-md-9">

19. {{ Form::open(array('url' => 'remind/reset/'.$token, 'method' => 'POST', 'class' => 'form-horizontal well')) }}

20. {{ Form::hidden('token', $token)}}

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

22. {{ Form::label('email', 'Mail :', array('class' => 'col-md-4 control-label')) }}

23. <div class="col-md-8">

24. {{ Form::text('email', '', $attributes = array('class' => 'form-control')) }}

25. </div>

26. </div>

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

28. {{ Form::label('password', 'Mot de passe :', array('class' => 'col-md-4 control-label')) }}

29. <div class="col-md-8">

30. {{ Form::password('password', $attributes = array('class' => 'form-control')) }}

31. </div>

32. </div>

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

34. {{ Form::label('password_confirmation', 'Confirmation passe :', array('class' => 'col-md-4 control-label')) }}

35. <div class="col-md-8">

36. {{ Form::password('password_confirmation', $attributes = array('class' => 'form-control')) }}

37. </div>

38. </div>

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

40. <div class="col-md-offset-4 col-md-8">

41. {{ Form::submit('Envoyer', array('class' => 'btn btn-success')) }}

42. </div>

43. </div>

44. {{ Form::close()}}

45. </div>

46. </div>

47. @stop

Le formulaire prend cet aspect :

Le traitement se fait dans la méthode postReset :

1. <?php

2. public function postReset()

3. {

4. $credentials = Input::only(

5. 'email', 'password', 'password_confirmation', 'token'

6. );

8. $response = Password::reset($credentials, function($user, $password)

9. {

10. $user->password = Hash::make($password);

12. $user->save();

13. });

15. switch ($response)

16. {

17. case Password::INVALID_PASSWORD:

18. case Password::INVALID_TOKEN:

19. case Password::INVALID_USER:

20. return Redirect::back()->with('error', Lang::get($response))->withInput();

22. case Password::PASSWORD_RESET:

23. return Redirect::to('auth/login');

24. }

25. }

Là est effectuée une validation. Par exemple on peut obtenir ce résultat :

On vérifie aussi si le jeton est valide, sinon on envoie aussi un message :

Si tout se passe bien, on renvoie sur la page de connexion pour que l'utilisateur puisse se connecter avec son nouveau mot de passe.

XXV - Chapitre 25 : Un blog : les tests unitaires

Nous allons voir dans ce chapitre un aspect important d'une application : les tests unitaires. Comme il est humainement impossible d'écrire un code sans erreur, il faut trouver un moyen efficace de les détecter. La méthode traditionnelle consiste à jouer l'utilisateur et à essayer une à une toutes les fonctionnalités. C'est long, laborieux et pas forcément complet. Heureusement il existe une autre possibilité qui consiste à automatiser ces tests. C'est justement le but des tests unitaires.

XXV-A - C'est quoi un test unitaire ?

Un test unitaire c'est un bout de code qui va vérifier qu'une fonctionnalité de votre application correspond bien au résultat attendu. En gros du code qui vérifie un autre code. Si le résultat est correct tout va bien, sinon vous recevez un message qui vous indique le problème. Le fait de diviser vos tests en petites unités présente de nombreux avantages : les résultats sont bien organisés et faciles à lire, on a une progression séquentielle des tests, on peut rendre certains tests dépendants d'autres tests…

XXV-A-1 - Que faut-il pour faire des tests ?

Laravel utilise le framework PHPUnit pour effectuer les tests. Ce framework est devenu de fait le standard dans ce domaine. Il est à la fois puissant et simple à utiliser. D'autre part Laravel 4 a été conçu dans le souci de rendre les tests faciles à réaliser avec PHPUnit. Pour utiliser ce framework vous devez l'installer. Il y a plusieurs possibilités mais la plus simple consiste à l'utiliser sous la forme d'un fichier phar que vous pouvez récupérer ici. Il vous suffit de placer ce fichier à la racine de votre application et il est disponible ! Laravel utilise aussi des composants de Symfony (HttpKernel, DomCrawler, et BrowserKit). D'autre part vous pouvez utiliser également Mockery. Dans ce chapitre, je vais me contenter de choses simples sans faire trop d'appels à ces composants. Ce choix n'est pas innocent et a de fortes implications, en particulier concernant Mockery, car ce dernier permet de simuler le fonctionnement de classes et d'ainsi pouvoir réellement isoler les tests.

XXV-A-2 - Principes de base de PHPUnit

Je vous conseille la lecture de la documentation au style très agréable et précis. Les éléments importants sont :

- un test pour un classe MaClasse va dans une classe MaClasseTest ;

- un test est une méthode publique dont le nom commence par test ;

- dans la méthode du test on utilise des méthodes d'assertion (par exemple assertTrue()) pour affirmer que le test est correct ;

- les tests sont à placer dans une architecture de dossiers (dans le répertoire app/tests pour Laravel) qui correspond à celle des classes à tester (notez qu'il y a à la racine de l'application un fichier phpunit.xml qui renseigne entre autres sur le dossier des tests) ;

- chaque test se fait dans un environnement nouveau avec un état connu, ce qui se nomme fixture du test. Lors de l'utilisation de tests, Laravel se met automatiquement en mode test (testing). Il y a un dossier app/config/testing destiné à recevoir la configuration pour les tests (cache, base…).

XXV-A-3 - Configurer la base de données pour les tests

Il n'est pas vraiment conseillé de faire les tests sur la base qui vous sert en production. Vous avez la possibilité de créer un fichier de configuration app/config/testing/database.php destiné à recevoir la configuration de la base pour la situation de test, et uniquement pour elle. La façon la plus efficace de procéder est d'utiliser SQLite en mode memory. Autrement dit la base est créée en mémoire vive et disparaît à l'issue du test. On y gagne en rapidité d'exécution. Mais il faut évidemment avoir les bonnes migrations et seeds pour générer la base et la remplir avec des données de test. Voici le contenu de ce fichier pour nos tests :

1. return array(

3. 'default' => 'sqlite',

5. 'connections' => array(

6. 'sqlite' => array(

7. 'driver'   => 'sqlite',

8. 'database' => ':memory:',

9. 'prefix'   => ''

10. ),

11. )

12. );

En mode test la base MySQL est ignorée et c'est une base SQLite qui est créée en mémoire vive.

XXV-A-4 - Les fichier TestCase

Le fichier app/test/TestCase contient l'initialisation de vos tests, si vous l'ouvrez vous trouvez cela :

1. class TestCase extends Illuminate\Foundation\Testing\TestCase {

3. /**

4. * Creates the application.

5. *

6. * @return Symfony\Component\HttpKernel\HttpKernelInterface

7. */

8. public function createApplication()

9. {

10. $unitTesting = true;

12. $testEnvironment = 'testing';

14. return require __DIR__.'/../../bootstrap/start.php';

15. }

17. }

On voit qu'une application est créée et que l'environnement se met en mode test ici. On va évidemment conserver ce code indispensable mais il nous faut créer la base, la remplir et prendre une dernière précaution : éviter d'envoyer des emails et plutôt générer un log. Voici le petit ajout :

1. class TestCase extends Illuminate\Foundation\Testing\TestCase {

3. /**

4. * Préparation avant les tests

5. */

6. public function setUp()

7. {

8. parent::setUp();

10. Artisan::call('migrate');

11. $this->seed();

12. Mail::pretend(true);

13. }

15. /**

16. * Creates the application.

17. *

18. * @return Symfony\Component\HttpKernel\HttpKernelInterface

19. */

20. public function createApplication()

21. {

22. $unitTesting = true;

24. $testEnvironment = 'testing';

26. return require __DIR__.'/../../bootstrap/start.php';

27. }

29. }

La méthode setUp est appelée avant chaque test unitaire. Maintenant nous sommes sûrs d'avoir une base de données fonctionnelle pour nos tests sans interférer sur la base de production .

Vous avez également un fichier app/test/ExampleTest.php que vous devez supprimer.

XXV-A-5 - Architecture des dossiers

Voici l'architecture avec nos contrôleurs :

Nous aurons donc la même architecture pour les tests :

Et voici le contenu initial pour ces fichiers :

class HomeControllerTest extends TestCase {}

class AuthControllerTest extends TestCase {}

class GuestControllerTest extends TestCase {}

Remarquez que ces classes dérivent de TestCase et qu'elles respectent la convention de nommage en finissant par Test. Pour le moment créez seulement le premier, pour éviter de recevoir un message de la part de PHPUnit dans la suite de notre démarche.

XXV-A-6 - Notre premier test

Il faut bien comprendre la philosophie des tests. Vous faites une action et vous attendez un certain résultat. Prenons cette action du contrôleur HomeController :

1. public function accueil()

2. {

3. $articles = Article::select('id', 'title', 'intro_text')

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

5. ->paginate(4);

6. return $this->gen_accueil($articles);

7. }

Cette action est activée par cette route :

Route::get('/', array('uses' => 'HomeController@accueil', 'as' => 'accueil'));

Autrement dit avec l'URL http://localhost/blog/public on s'attend à voir apparaître la page d'accueil du blog. Nous allons créer un test pour vérifier que ça fonctionne bien :

1. class HomeControllerTest extends TestCase {

3. public function testAccueil()

4. {

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

6. $this->assertResponseOk();

7. }

8. }

Le test s'appelle testAccueil (il commence forcément par test). La première instruction génère la requête et récupère la réponse. La seconde instruction est une assertion qui vérifie qu'on reçoit une réponse correcte. Lançons maintenant PHPUnit :

On trouve logiquement un test et une assertion, avec un résultat positif. Que nous apprend ce test ? Que le chemin est bon, qu'on renvoie une réponse correcte, mais on n'a aucune idée du contenu de la réponse. C'est ici qu'il faut savoir ce qu'on désire réellement tester et jusqu'où aller. Soyons un peu curieux et voyons ce que nous offre cette réponse :

echo var_dump($response->getContent());

On se retrouve avec le contenu de la page HTML retournée. Ce qui peut être utile.

Nous savons aussi que nous transmettons trois paramètres à la vue, et nous pouvons tester cette transmission ainsi :

1. public function testAccueil()

2. {

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

4. $this->assertViewHas(array('categories', 'articles', 'actif'));

5. $this->assertResponseOk();

6. }

Cette fois nous avons quatre assertions, une pour chaque paramètre et une pour la réponse.

Vous pouvez aussi utiliser le crawler (robot d'indexation) pour explorer le DOM, et filtrer des informations :

1. public function testAccueil()

2. {

3. $crawler = $this->client->request('GET', '/');

4. $this->assertTrue($this->client->getResponse()->isOk());

5. $this->assertCount(1, $crawler->filter('h1:contains("Mon joli blog !")'));

6. }

Vous avez la documentation du crawler ici.

XXV-A-7 - Les tests pour HomeController

XXV-A-7-a - Actions « accueil », « categorie » et « article »

Maintenant que nous nous sommes échauffés, passons en revue les actions du contrôleur HomeController pour écrire les tests correspondants. Il y a trois actions de même nature pour lesquelles nous allons nous contenter de vérifier la réponse correcte :

1. public function testAccueil()

2. {

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

4. $this->assertResponseOk();

5. }

7. public function testCategorie()

8. {

9. $response = $this->call('GET', 'cat/1');

10. $this->assertResponseOk();

11. }

13. public function testArticle()

14. {

15. $response = $this->call('GET', 'art/1/1');

16. $this->assertResponseOk();

17. }

XXV-A-7-b - Action « find »

Maintenant il nous faut réfléchir à une action un peu particulière, celle de la recherche à partir du formulaire prévu à cet effet. Il nous faut définir deux tests : un avec le résultat de recherche avec des articles trouvés ou pas, et l'autre négatif en cas de champ de recherche vide. Voyons pour le test positif. Nous devons trouver un terme de recherche, simuler l'envoi du formulaire et voir si la réponse est correcte :

1. public function testFindOk()

2. {

3. $article = Article::find(1);

4. $find = $article->intro_text;

5. // Envoi du formulaire

6. $response = $this->call('POST', 'find', array('find' => $find));

7. // Assertion

8. $this->assertContains('<p>'.$find.'</p>', $response->getContent());

9. }

Voyons un peu où nous en sommes :

Voyons à présent le cas du champ vide :

1. public function testFindNok()

2. {

3. // Envoi du formulaire

4. $response = $this->call('POST', 'find');

5. // Assertions

6. $this->assertRedirectedTo('/');

7. $this->assertSessionHas('flash_error', 'Il faudrait entrer un terme pour la recherche !');

8. }

Cette fois on envoie un formulaire sans le champ. Vous voyez l'utilisation de la méthode assertRedirectedTo pour tester la redirection et assertSessionHas pour vérifier la présence d'une information de session.

XXV-A-7-c - Action « comment »

Il ne nous manque plus qu'à tester l'action « comment » pour en finir avec ce contrôleur :

1. public function testComment()

2. {

3. // Authentification

4. $this->be(User::find(1));

5. // Envoi du formulaire

6. $response = $this->call('POST', 'comment',

7. array(

8. 'title' => 'titre à tester',

9. 'comment' => 'commentaire',

10. 'id_art' => 1,

11. 'id_cat' => 1

12. )

13. );

14. // Assertions

15. $this->assertEquals(1, Comment::where('title', 'titre à tester')->count());

16. $this->assertRedirectedTo('art/1/1');

17. }

Notez qu'on doit procéder dans un premier temps à l'authentification avec la méthode be parce que seuls les utilisateurs connectés peuvent laisser un commentaire. On voit aussi apparaître la méthode assertEquals qui permet de tester une égalité, ça nous sert pour vérifier que l'information a bien été mémorisée dans la base.

XXV-A-8 - Les tests pour AuthController

Ici ça ira vite étant donné qu'il n'y a que l'action « logout » :

1. class AuthControllerTest extends TestCase {

3. public function testLogout()

4. {

5. // Authentification

6. $this->be(User::find(1));

7. $this->assertTrue(Auth::check());

8. // Requête

9. $response = $this->call('GET', 'auth/logout');

10. // Assertions

11. $this->assertTrue(Auth::guest());

12. $this->assertRedirectedToRoute('accueil');

13. }

15. }

On commence par authentifier l'utilisateur avec la méthode be. On vérifie que c'est bien fait. Ensuite on génère la requête et on vérifie que l'utilisateur n'est plus authentifié. Il ne nous reste plus qu'à vérifier la redirection. Voyons où nous en sommes :

XXV-A-9 - Les tests pour GuestController

Ici nous avons un peu plus de travail. Mais vous avez compris le principe, je vous livre donc le code sans le commenter :

1. class GuestControllerTest extends TestCase {

3. public function testGetLogin()

4. {

5. $response = $this->call('GET', 'guest/login');

6. $this->assertResponseOk();

7. }

9. public function testGetReset()

10. {

11. $response = $this->call('GET', 'guest/reset/token');

12. $this->assertResponseOk();

13. }

15. public function testGetOubli()

16. {

17. $response = $this->call('GET', 'guest/oubli');

18. $this->assertResponseOk();

19. }

21. public function testGetInscription()

22. {

23. $response = $this->call('GET', 'guest/inscription');

24. $this->assertResponseOk();

25. }

27. public function testPostLoginOk()

28. {

29. $this->assertTrue(Auth::guest());

30. // Envoi du formulaire

31. $response = $this->call('POST', 'guest/login',

32. array(

33. 'username' => 'admin',

34. 'password' => 'admin'

35. )

36. );

37. // Assertions

38. $this->assertTrue(Auth::check());

39. $this->assertRedirectedToRoute('accueil');

40. $this->assertSessionHas('flash_notice', 'Vous avez été correctement connecté avec le pseudo ' . Auth::user()->username);

41. }

43. public function testPostLoginNok()

44. {

45. // Envoi du formulaire

46. $response = $this->call('POST', 'guest/login');

47. // Assertions

48. $this->assertTrue(Auth::guest());

49. $this->assertRedirectedTo('guest/login');

50. $this->assertSessionHas('flash_error', 'Pseudo ou mot de passe non correct !');

51. }

53. public function testPostOubliOk()

54. {

55. $user = User::find(1);

56. // Envoi du formulaire

57. $response = $this->call('POST', 'guest/oubli', array('email' => $user->email));

58. // Assertions

59. $this->assertRedirectedTo('guest/oubli');

60. $this->assertSessionHas('flash_notice', 'Un mail vous a été envoyé, veuillez suivre ses indications pour renouveler votre mot de passe.');

61. }

63. public function testPostOubliNok()

64. {

65. // Envoi du formulaire

66. $response = $this->call('POST', 'guest/oubli');

67. $this->assertRedirectedTo('guest/oubli');

68. }

70. public function testPostInscriptionOk()

71. {

72. // Envoi du formulaire

73. $response = $this->call('POST', 'guest/inscription',

74. array(

75. 'username' => 'Dupontel',

76. 'password' => 'jolipasse',

77. 'email' => 'mail@moi.com',

78. 'password_confirmation' => 'jolipasse'

79. )

80. );

81. // Assertions

82. $this->assertEquals(1, User::where('username', 'Dupontel')->count());

83. $this->assertRedirectedToRoute('accueil');

84. $this->assertSessionHas('flash_notice', 'Votre compte a été créé.');

85. }

87. public function testPostInscriptionNok()

88. {

89. // Envoi du formulaire

90. $response = $this->call('POST', 'guest/inscription');

91. // Assertion

92. $this->assertRedirectedTo('guest/inscription');

93. }

95. public function testPostResetOk()

96. {

97. $token = sha1('token');

98. $user = User::find(1);

99. // Mot de passe initial

100. $passe1 = $user->password;

101. // Création du token dans la base

102. DB::table('password_reminders')->insert(

103. array(

104. 'email' => $user->email,

105. 'token' => $token,

106. 'created_at' => new DateTime)

107. );

108. // Envoi du formulaire

109. $response = $this->call('POST', 'guest/reset/'.$token,

110. array(

111. 'token' => $token,

112. 'mail' => $user->email,

113. 'password' => 'jolipasse',

114. 'password_confirmation' => 'jolipasse'

115. )

116. );

117. // Nouveau mot de passe

118. $user = User::find(1);

119. $passe2 = $user->password;

120. // Assertions

121. $this->assertNotEquals($passe1, $passe2);

122. $this->assertRedirectedToRoute('accueil');

123. }

125. public function testPostResetNok()

126. {

127. // Envoi du formulaire

128. $response = $this->call('POST', 'guest/reset/test');

129. // Assertions

130. $this->assertRedirectedTo('guest/reset/test');

131. }

132. }

L'action la plus délicate à tester concerne la réinitialisation du mot de passe. Il faut en effet renseigner la base avec un token (jeton) pour que tout se déroule bien. Lançons une dernière fois PHPUnit :

Remarquez l'information sur l'envoi d'un email.

On pourrait évidemment imaginer ces tests d'une façon différente. Le but de cette partie est juste de vous montrer comment écrire des tests pour Laravel 4. Ces tests sont importants, ne les négligez pas. Il existe même une démarche de développement dirigé par les tests et même de développement dirigé par le comportement. On peut même aller plus loin et générer le squelette des classes à partir des tests…

XXVI - Chapitre 26 : Création d'un paquetage

Pour ajouter des fonctionnalités à Laravel le plus simple est de créer un paquetage. D'ailleurs Laravel est lui-même un paquetage. Vous trouverez tous les paquetage utilisables avec composer sur le site Packagist. Par exemple on trouve Laravel ici sur ce site. Certains des paquetages sont destinés spécifiquement à Laravel 4, la plupart ne le sont pas spécialement mais peuvent évidemment tout de même être utilisés avec Laravel.

Dans Laravel 3 on ajoutait des bundles, pour Laravel 4 c'est donc différent, il faut créer des paquetages qui sont reconnus valides par composer. Je vous propose un exemple simple. Laravel possède une classe Form qui permet de créer facilement des formulaires, nous l'avons déjà vue et utilisée. Nous allons considérer que nous désirons les nouvelles possibilités du HTML5 au niveau des balises <input> avec les types color, date, etc. En effet la classe Form ne nous propose que le type email parmi ces nouvelles possibilités (mon exemple est un peu artificiel parce qu'on peut utiliser la méthode input de Form pour obtenir tous les types, mais comme ça on ne va pas se perdre dans le code du paquetage qui va rester élémentaire).

Commencez par créer une nouvelle installation de Laravel 4. Vous devez obtenir cette arborescence :

XXVI-A - Le workbench

Laravel nous offre un outil pratique pour créer un paquetage: le workbench (plan de travail). Mais il va nous falloir d'abord renseigner deux éléments (nom et adresse email) dans le fichier app/config/workbench.php :

1. <?php

3. return array(

5. /*

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

7. | Workbench Author Name

8. |--------------------------------------------------------------------------

9. |

10. | When you create new packages via the Artisan "workbench" command your

11. | name is needed to generate the composer.json file for your package.

12. | You may specify it now so it is used for all of your workbenches.

13. |

14. */

16. 'name' => 'Bestmomo',

18. /*

19. |--------------------------------------------------------------------------

20. | Workbench Author E-Mail Address

21. |--------------------------------------------------------------------------

22. |

23. | Like the option above, your e-mail address is used when generating new

24. | workbench packages. The e-mail is placed in your composer.json file

25. | automatically after the package is created by the workbench tool.

26. |

27. */

29. 'email' => 'bestmomo@la.fr',

31. );

Une fois que c'est fait on peut utiliser le workbench :

La syntaxe est simple : le « vendor » est ici « bestmomo » et le paquetage s'appelle « formulor ». Regardons ce que le workbench a créé :

On a un nouveau dossier workbench/bestmomo/formulor qui contient quelques fichiers dont l'indispensable composer.json, un dossier src pour recevoir le code du paquetage avec pour le moment un seul fichier : FormulorServiceProvider.php, un dossier tests, et un dossier vendor avec composer et illuminate.

Par défaut le workbench crée un paquetage « agnostic », c'est-à-dire qu'il n'est pas spécialement adapté à Laravel. Pour obtenir un paquetage spécialement adapté à Laravel il faut ajouter l'option -resources, ce qui a pour effet de créer les dossiers config, lang, views, migrations… Dans notre cas on n'a pas besoin de ces dossiers, je n'ai donc pas utilisé cette option.

XXVI-A-1 - Le fichier FormulorServiceProvider.php

Voici ce fichier tel qu'il a été créé par le workbench :

1. <?php namespace Bestmomo\Formulor;

3. use Illuminate\Support\ServiceProvider;

5. class FormulorServiceProvider extends ServiceProvider {

7. /**

8. * Indicates if loading of the provider is deferred.

9. *

10. * @var bool

11. */

12. protected $defer = false;

14. /**

15. * Register the service provider.

16. *

17. * @return void

18. */

19. public function register()

20. {

21. //

22. }

24. /**

25. * Get the services provided by the provider.

26. *

27. * @return array

28. */

29. public function provides()

30. {

31. return array();

32. }

34. }

Il faut donc renseigner la méthode register qui est appelée dès que le paquetage est enregistré, et la méthode provides destinée à fournir le service :

1. /**

2. * Register the service provider.

3. *

4. * @return void

5. */

6. public function register()

7. {

8. $this->app['formulor'] = $this->app->share(function($app)

9. {

10. return new Formulor;

11. });

12. }

14. /**

15. * Get the services provided by the provider.

16. *

17. * @return array

18. */

19. public function provides()

20. {

21. return array('formulor');

22. }

La propriété app est une instance du conteneur de l'application. Ici on crée une liaison entre notre paquetage et l'application.

Il nous faut maintenant enregistrer notre provider dans le fichier app/config/app.php :

1. 'Illuminate\View\ViewServiceProvider',

2. 'Illuminate\Workbench\WorkbenchServiceProvider',

3. 'Bestmomo\Formulor\FormulorServiceProvider'

4. ),

XXVI-A-2 - Le code de formulor

Il nous faut maintenant coder notre formulor. Créons juste deux types color et date. On crée un fichier Bestmomo/Formulor/Formulor.php pour contenir notre classe :

1. <?php namespace Bestmomo\Formulor;

3. use Form;

5. class Formulor {

7. /**

8. * Create a color input field.

9. *

10. * @param  string  $name

11. * @param  string  $value

12. * @param  array   $options

13. * @return string

14. */

15. public function color($name, $value = null, $options = array())

16. {

17. return Form::input('color', $name, $value, $options);

18. }

20. /**

21. * Create a date input field.

22. *

23. * @param  string  $name

24. * @param  string  $value

25. * @param  array   $options

26. * @return string

27. */

28. public function date($name, $value = null, $options = array())

29. {

30. return Form::input('date', $name, $value, $options);

31. }

32. }

Testons tout de suite ça en utilisant ce code dans le fichier app/routes.php :

echo $app['formulor']->color('couleur');

Commentons aussi le contenu de la route définie par défaut par Laravel. Vous obtenez ce résultat :

<input name="couleur" type="color">

Pour le moment tout a l'air de bien se passer .

XXVI-A-3 - La façade

On va maintenant créer la façade pour avoir un accès statique à notre classe. On crée un fichier Bestmomo/Formulor/FormulorFacade.php avec ce code :

1. <?php namespace Bestmomo\Formulor;

3. use Illuminate\Support\Facades\Facade;

5. class FormulorFacade extends Facade {

7. /**

8. * Get the registered name of the component.

9. *

10. * @return string

11. */

12. protected static function getFacadeAccessor() { return 'formulor'; }

14. }

Et évidemment on déclare la façade dans le fichier app/config/app.php :

1. 'Validator'       => 'Illuminate\Support\Facades\Validator',

2. 'View'            => 'Illuminate\Support\Facades\View',

3. 'Formulor'        => 'Bestmomo\Formulor\FormulorFacade'

4. ),

Du coup on peut utiliser maintenant ce code dans le fichier app/routes.php :

echo Formulor::color('couleur');

XXVI-A-4 - Le fichier composer.json

Voyons à présent ce fichier généré par workbench :

1. {

2. "name": "bestmomo/formulor",

3. "description": "",

4. "authors": [

5. {

6. "name": "Bestmomo",

7. "email": "bestmomo@la.fr"

8. }

9. ],

10. "require": {

11. "php": ">=5.3.0",

12. "illuminate/support": "4.0.x"

13. },

14. "autoload": {

15. "psr-0": {

16. "Bestmomo\\Formulor": "src/"

17. }

18. },

19. "minimum-stability": "dev"

20. }

Nous voyons qu'il nous faut compléter la description, faisons-le :

"description": "Des formulaires plus complets",

Voyons maintenant si le fichier est valide pour composer :

On nous reproche juste de ne pas avoir précisé de licence, complétons encore :

"license": "BSD-2-Clause",

Et maintenant :

Notre paquetage est valide, nous pouvons le proposer . Pour ce faire il faudrait déjà le placer dans Github, et ensuite le proposer à Composer.

XXVI-A-5 - Utilisation du paquetage directement

Vous n'êtes évidemment pas obligé de mettre votre paquetage à disposition, vous pouvez l'utiliser directement dans une application. Il suffit de créer un dossier vendor/bestmomo/formulor, avec les trois classes :

Il faut ensuite renseigner le fichier app/composer.json :

1. {

2. "require": {

3. "laravel/framework": "4.0.*"

4. },

5. "autoload": {

6. "classmap": [

7. "app/commands",

8. "app/controllers",

9. "app/models",

10. "app/database/migrations",

11. "app/database/seeds",

12. "app/tests/TestCase.php",

13. "vendor/bestmomo/formulor"

14. ]

15. },

16. "scripts": {

17. "pre-update-cmd": [

18. "php artisan clear-compiled"

19. ],

20. "post-update-cmd": [

21. "php artisan optimize"

22. ]

23. },

24. "config": {

25. "preferred-install": "dist"

26. },

27. "minimum-stability": "dev"

28. }

Ainsi nos classes seront prises en compte, mais il faut évidemment actualiser l'autoload (avec composer dumpautoload). On termine en renseignant le fichier app/config/app.php pour enregistrer le fournisseur et déclarer la façade comme nous l'avons fait précédemment.

XXVI-A-6 - Un packetage avec CKEditor

Maintenant que nous avons vu comment créer un paquetage avec un exemple simple, voyons un cas plus réaliste. Vous connaissez sans doute l'éditeur CKEditor. C'est un éditeur Web puissant et esthétique. Son intégration à une page Web est facile et il comporte une multitude d'options. Nous allons créer un packetage pour rendre son intégration dans une vue encore plus facile.

Commencez par créer un workbench comme nous l'avons vu ci-dessus mais cette fois pour bestmomo/ckeditor. Voici le code pour le fichier Bestmomo/Ckeditor/Ckeditor.php :

1. <?php namespace Bestmomo\Ckeditor;

3. use Form, HTML;

5. class Ckeditor {

7. /**

8. * Variable booléenne pour indiquer si on a déjà créé un éditeur

9. */

10. private $initialized = false;

12. /**

13. * Création d'un éditeur

14. *

15. * @param $name (string) Nom de l'éditeur, et aussi le nom du textarea

16. * @param $value (string) Valeur initiale (optionnel).

17. * @param $config (array) Configuration pour cet éditeur (optionnel).

18. *

19. * Exemples :

20. *

21. * $CKEditor = Ckeditor::make("editor1", "<p>Une valeur</p>");

22. *

23. * $CKEditor = Ckeditor::make("editor1", "<p>Une valeur</p>", , array('toolbar' => 'Basic', 'uiColor' => '#9AB8F3'));

24. *

25. */

26. public function make($name, $value = '', $config = array())

27. {

28. $out = '';

29. if(!$this->initialized) {

30. $this->initialized = true;

31. $out = HTML::script('ckeditor/ckeditor.js');

32. }

33. $out .= Form::textarea($name, htmlspecialchars($value));

34. $out .= "<script> CKEDITOR.replace( \"$name\"";

35. if (!empty($config)) $out .= ',' . json_encode($config);

36. $out .= " ); </script>\n";

37. return $out;

38. }

40. }

La variable $initialized est destinée à ne référencer qu'une fois la bibliothèque JavaScript si on met plusieurs éditeurs sur la même page. La méthode make génère le code nécessaire pour l'intégration d'un éditeur avec la possibilité d'avoir une valeur, et aussi de configurer l'éditeur.

Voici maintenant le fichier Bestmomo/Ckeditor/CkeditorServiceProvider :

1. <?php namespace Bestmomo\Ckeditor;

3. use Illuminate\Support\ServiceProvider;

5. class CkeditorServiceProvider extends ServiceProvider {

7. /**

8. * Indicates if loading of the provider is deferred.

9. *

10. * @var bool

11. */

12. protected $defer = false;

14. /**

15. * Bootstrap the application events.

16. *

17. * @return void

18. */

19. public function boot()

20. {

21. $this->package('bestmomo/ckeditor');

22. }

24. /**

25. * Register the service provider.

26. *

27. * @return void

28. */

29. public function register()

30. {

31. $this->app['ckeditor'] = $this->app->share(function($app)

32. {

33. return new Ckeditor;

34. });

35. }

37. /**

38. * Get the services provided by the provider.

39. *

40. * @return array

41. */

42. public function provides()

43. {

44. return array();

45. }

47. }

Et voici la façade Bestmomo/Ckeditor/CkeditorFacade :

1. <?php namespace Bestmomo\Ckeditor;

3. use Illuminate\Support\Facades\Facade;

5. class CkeditorFacade extends Facade {

7. /**

8. * Get the registered name of the component.

9. *

10. * @return string

11. */

12. protected static function getFacadeAccessor() { return 'ckeditor'; }

14. }

Renseignez ensuite le fichier app/config/app.php :

1. 'Illuminate\View\ViewServiceProvider',

2. 'Illuminate\Workbench\WorkbenchServiceProvider',

3. 'Bestmomo\Ckeditor\CkeditorServiceProvider'

4. ),

6. ...

8. 'Validator'       => 'Illuminate\Support\Facades\Validator',

9. 'View'            => 'Illuminate\Support\Facades\View',

10. 'Ckeditor'        => 'Bestmomo\Ckeditor\CkeditorFacade'

11. ),

Pour tester tout ça, créez une vue app/views/home.blade.php :

1. <!DOCTYPE html>

2. <head>

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

4. <title>Test de CKeditor</title>

5. </head>

7. <body>

8. {{ $editor }}

9. </body>

Et enfin une route :

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

2. {

3. $editor1 = Ckeditor::make('test1', 'texte1', array('toolbar' => 'Basic', 'uiColor' => '#9AB8F3'));

4. $editor2 = Ckeditor::make('test2', 'texte2');

5. return View::make('home')->with('editor', $editor1.$editor2);

6. });

Normalement vous obtenez ce résultat :

Vous trouverez un exemple de paquetage plus complet mais en anglais ici.

XXVII - Chapitre 27 : Cache et configuration

Voyons aujourd'hui la mise en cache à la sauce Laravel 4. Je rappelle qu'un cache est destiné à accélérer la génération des pages en gardant en mémoire des informations. Le cas le plus classique est celui de requêtes sur une base de données. Si les données ne changent pas fréquemment il est plus efficace de mettre en cache leur résultat plutôt que d'aller chaque fois interroger la base. On peut aussi carrément mettre en cache des pages complètes.

Laravel 4 propose un système de cache simple et efficace qui mémorise par défaut les informations dans un fichier. Regardez le fichier app/config/cache.php :

1. return array(

3. /*

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

5. | Default Cache Driver

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

7. |

8. | This option controls the default cache "driver" that will be used when

9. | using the Caching library. Of course, you may use other drivers any

10. | time you wish. This is the default when another is not specified.

11. |

12. | Supported: "file", "database", "apc", "memcached", "redis", "array"

13. |

14. */

16. 'driver' => 'file',

18. /*

19. |--------------------------------------------------------------------------

20. | File Cache Location

21. |--------------------------------------------------------------------------

22. |

23. | When using the "file" cache driver, we need a location where the cache

24. | files may be stored. A sensible default has been specified, but you

25. | are free to change it to any other place on disk that you desire.

26. |

27. */

29. 'path' => storage_path().'/cache',

31. /*

32. |--------------------------------------------------------------------------

33. | Database Cache Connection

34. |--------------------------------------------------------------------------

35. |

36. | When using the "database" cache driver you may specify the connection

37. | that should be used to store the cached items. When this option is

38. | null the default database connection will be utilized for cache.

39. |

40. */

42. 'connection' => null,

44. /*

45. |--------------------------------------------------------------------------

46. | Database Cache Table

47. |--------------------------------------------------------------------------

48. |

49. | When using the "database" cache driver we need to know the table that

50. | should be used to store the cached items. A default table name has

51. | been provided but you're free to change it however you deem fit.

52. |

53. */

55. 'table' => 'cache',

57. /*

58. |--------------------------------------------------------------------------

59. | Memcached Servers

60. |--------------------------------------------------------------------------

61. |

62. | Now you may specify an array of your Memcached servers that should be

63. | used when utilizing the Memcached cache driver. All of the servers

64. | should contain a value for "host", "port", and "weight" options.

65. |

66. */

68. 'memcached' => array(

70. array('host' => '127.0.0.1', 'port' => 11211, 'weight' => 100),

72. ),

74. /*

75. |--------------------------------------------------------------------------

76. | Cache Key Prefix

77. |--------------------------------------------------------------------------

78. |

79. | When utilizing a RAM based store such as APC or Memcached, there might

80. | be other applications utilizing the same cache. So, we'll specify a

81. | value to get prefixed to all our keys so we can avoid collisions.

82. |

83. */

85. 'prefix' => 'laravel',

87. );

Vous trouvez toutes les possibilités de configuration du cache. Vous voyez en premier la façon dont les informations sont stockées :

- fichier (valeur par défaut) ;

- base de données ;

- APC ;

- Memcached ;

- Redis ;

- array.

Vous trouvez aussi la localisation du fichier (par défaut app/storage/cache) ; ainsi que : le nom de la base et de la table à utiliser dans le cas de stockage en base de données, le paramétrage de memcached, ; le préfixe pour APC ou Memcached.

Quelle que soit la méthode choisie, la classe Cache de Laravel offre les mêmes possibilités de base.

XXVII-A - Les commandes de base

Avec une nouvelle installation de Laravel 4 entrez ce code dans le fichier des routes :

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

2. {

3. Cache::put('un', '1', 2);

4. echo Cache::get('un');

5. });

Résultat :

1

La méthode put permet de mémoriser une information : le premier paramètre est la clé, le second la valeur et le troisième la durée de conservation de l'information.

La méthode get est l'inverse, et permet de retrouver une information mémorisée à partir de sa clé.

Regardons maintenant le fichier créé :

Et son contenu :

1368103994s:1:"1";

Il est possible de conserver indéfiniment une information en utilisant la méthode forever :

Cache::forever('un', '1');

Et la méthode forget pour en supprimer une :

Cache::forget('un');

On a aussi la possibilité d'affecter une valeur par défaut en cas d'inexistence de la valeur :

Cache::get('deux', function() { return '2'; });

Vous pouvez aussi récupérer une valeur dans le cache et la mémoriser si elle n'est pas présente :

1. $value = Cache::remember('un', 1, function()

2. {

3. return 1;

4. });

XXVII-A-1 - Un cas d'application

Voyons maintenant un cas d'application qui justifie l'utilisation d'un cache. Si vous allez sur cette page vous trouvez la documentation de Laravel 4. Cette application a été réalisée par Dayle Rees et le code se trouve ici. Je me suis dit qu'il y avait une façon plus simple de procéder en allant directement chercher les informations sur la page du projet. Surtout qu'il y a une page avec la table des matières.

XXVII-A-1-a - La route

Partez d'une nouvelle installation de Laravel. Dans le fichier des routes mettez juste ce code pour appeler un contrôleur :

Route::get('/{name?}', 'DocController@show');

XXVII-A-1-b - Le contrôleur

Voici le code du contrôleur app/controllers/DocController.php :

1. <?php

3. class DocController extends BaseController {

5. /**

6. * Le template de base

7. */

8. protected $layout = 'layout.master';

10. /**

11. * Récupération du HTML d'un URL

12. */

13. private function getHtml($url)

14. {

15. $curl = curl_init($url);

16. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

17. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);

18. $output = curl_exec($curl);

19. curl_close($curl);

20. return $output;

21. }

23. /**

24. * Récupération d'une balise dans du HTML

25. */

26. private function getTag($html, $tag, $exclu = null)

27. {

28. $start = strpos($html, '<' . $tag);

29. $end = strpos($html, '</' . $tag);

30. if($exclu == null) {

31. return substr($html, $start, $end - $start + strlen($tag) + 2);

32. } else {

33. $start = strpos($html, '>', $start) + 1;

34. return substr($html, $start, $end - $start);

35. }

36. }

38. /**

39. * Affichage de la page.

40. */

41. public function show($name = 'introduction')

42. {

43. // Récupération de la page d'index

44. $index = $this->getHtml('https://github.com/laravel/docs/blob/master/documentation.md');

46. // Sélection de l'index

47. $index = $this->getTag($index, 'article', true);

49. // Création du DOM

50. libxml_use_internal_errors(true);

51. $doc = new DOMDocument;

52. $doc->loadHTML($index);

54. // Changement des liens

55. $nodes = $doc->getElementsByTagName('a');

56. foreach ($nodes as $node) {

57. $link = $node->getAttribute('href');

58. $node->setAttribute('href', basename($link));

59. $node->setAttribute('class', 'btn btn-small');

60. }

62. // Création du Html élagué

63. return $this->getTag($doc->saveHTML(), 'body', true);

65. // Récupération du contenu

66. $content = $this->getHtml('https://github.com/laravel/docs/blob/master/' . $name . '.md');

68. // Sélection du texte

69. $content = $this->getTag($content, 'article');

71. // Création du DOM

72. $doc->loadHTML($content);

74. // Ajout de la classe pour le prettify

75. $nodes = $doc->getElementsByTagName('pre');

76. foreach ($nodes as $node) {

77. $node->setAttribute('class', 'prettyprint');

78. }

80. // Ajout classes pour les tableaux

81. $nodes = $doc->getElementsByTagName('table');

82. foreach ($nodes as $node) {

83. $node->setAttribute('class', 'table table-bordered table-condensed');

84. }

86. // Création du Html élagué

87. $content = $this->getTag($doc->saveHTML(), 'body', true);

89. // Affichage de la vue

90. $this->layout->content = View::make('main', array('nav' => $nav, 'content' => $content));

91. }

93. }

En gros le code permet d'aller récupérer le lien HTML de la documentation, de sélectionner juste ce qui est utile, de modifier les liens et d'ajouter quelques classes pour la présentation. Je laisse des classes devenue inutiles, les puristes peuvent les supprimer…

XXVII-A-1-c - Les vues

Créez le template app/views/layout/master.blade.php :

1. <!DOCTYPE html>

2. <head>

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

4. <title>Laravel documentation</title>

5. <meta name="viewport" content="width=device-width, initial-scale=1.0">

6. {{ HTML::style('assets/css/bootstrap.min.css') }}

7. {{ HTML::style('assets/css/bootstrap-responsive.min.css') }}

8. {{ HTML::style('assets/css/main.css') }}

9. </head>

11. <body>

12. <div class="container">

13. <h1>Documentation de Laravel 4</h1>

14. <hr>

15. <div class="row">

16. <nav class="span3">

17. @yield('navigation')

18. </nav>

19. <section class="span9">

20. @yield('content')

21. </section>

22. </div>

23. </div>

24. {{ HTML::script('assets/javascript/run_prettify.js') }}

25. </body>

On charge Bootstrap pour la mise en forme, on prévoit deux localisations pour la navigation et le contenu et on charge Prettify pour la coloration syntaxique du code.

On crée ensuite la vue app/views/main.blade.php qui utilise ce template :

1. @section('navigation')

2. {{ $nav }}

3. @stop

5. @section('content')

6. {{ $content }}

7. @stop

XXVII-A-1-d - Les assets

Il ne nous manque plus que les assets :

Pour télécharger Bootstrap c'est ici. Et pour Prettify c'est ici. Et voici le code de main.css :

1. body {

2. background-color: #ccc;

3. }

4. .container {

5. margin-top: 20px;

6. }

7. pre {

8. background-color: white;

9. }

10. ul {

11. list-style-type: none;

12. }

13. nav ul:first-child {

14. font-size: 20px;

15. padding: 10px;

16. color: #27a;

17. }

Et voici l'apparence du résultat :

Les liens fonctionnent et la présentation est agréable ( à mon goût, mais vous pouvez changer tout ce que vous voulez ).

XXVII-A-2 - Mise en cache pour l'application

Maintenant que nous avons une application plutôt laborieuse parce qu'elle doit envoyer des requêtes HTTP pour récupérer des informations, nous pouvons utiliser un cache pour la rendre bien plus rapide. Il faut mettre en cache d'une part la table des matières, et ensuite chacun des chapitres au fur et à mesure qu'ils sont affichés. Voici le contrôleur modifié en conséquence :

1. class DocController extends BaseController {

3. /**

4. * Le template de base

5. */

6. protected $layout = 'layout.master';

8. /**

9. * Récupération du HTML d'un URL

10. */

11. private function getHtml($url)

12. {

13. $curl = curl_init($url);

14. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

15. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);

16. $output = curl_exec($curl);

17. curl_close($curl);

18. return $output;

19. }

21. /**

22. * Récupération d'une balise dans du HTML

23. */

24. private function getTag($html, $tag, $exclu = null)

25. {

26. $start = strpos($html, '<' . $tag);

27. $end = strpos($html, '</' . $tag);

28. if($exclu == null) {

29. return substr($html, $start, $end - $start + strlen($tag) + 2);

30. } else {

31. $start = strpos($html, '>', $start) + 1;

32. return substr($html, $start, $end - $start);

33. }

34. }

36. /**

37. * Affichage de la page

38. */

39. public function show($name = 'introduction')

40. {

41. // Récupération de l'index

42. $nav = Cache::remember('nav', 60, function()

43. {

44. // Récupération de la page d'index

45. $index = $this->getHtml('https://github.com/laravel/docs/blob/master/documentation.md');

47. // Sélection de l'index

48. $index = $this->getTag($index, 'article', true);

50. // Création du DOM

51. libxml_use_internal_errors(true);

52. $doc = new DOMDocument;

53. $doc->loadHTML($index);

55. // Changement des liens

56. $nodes = $doc->getElementsByTagName('a');

57. foreach ($nodes as $node) {

58. $link = $node->getAttribute('href');

59. $node->setAttribute('href', basename($link));

60. $node->setAttribute('class', 'btn btn-small');

61. }

63. // Création du Html élagué

64. return $this->getTag($doc->saveHTML(), 'body', true);

65. });

67. // Récupération du contenu

68. $content = Cache::remember($name, 60, function() use ($name)

69. {

70. $content = $this->getHtml('https://github.com/laravel/docs/blob/master/' . $name . '.md');

72. // Sélection du texte

73. $content = $this->getTag($content, 'article');

75. // Création du DOM

76. libxml_use_internal_errors(true);

77. $doc = new DOMDocument;

78. $doc->loadHTML($content);

80. // Ajout de la classe pour le prettify

81. $nodes = $doc->getElementsByTagName('pre');

82. foreach ($nodes as $node) {

83. $node->setAttribute('class', 'prettyprint');

84. }

86. // Ajout classes pour les tableaux

87. $nodes = $doc->getElementsByTagName('table');

88. foreach ($nodes as $node) {

89. $node->setAttribute('class', 'table table-bordered table-condensed');

90. }

92. // Création du Html élagué

93. return $this->getTag($doc->saveHTML(), 'body', true);

94. });

96. // Affichage de la vue

97. $this->layout->content = View::make('main', array('nav' => $nav, 'content' => $content));

98. }

100. }

Maintenant notre application devient performante. J'ai réglé la durée de mémorisation à 60 minutes, ce qui semble très raisonnable dans ce cas parce que la documentation change rarement.

XXVII-A-3 - Configuration

On peut améliorer l'ergonomie de notre application en utilisant un fichier de configuration pour la durée de mémorisation et le titre de la page. Créez un fichier app/config/doc.php avec ce code :

1. return array(

3. /*

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

5. | Titre

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

7. |

8. | Titre qui apparaît en haut de la page

9. |

10. | Défaut : 'Documentation de Laravel 4'

11. |

12. */

14. 'title' => 'Documentation de Laravel 4',

16. /*

17. |--------------------------------------------------------------------------

18. | Durée

19. |--------------------------------------------------------------------------

20. |

21. | Durée de mémorisation pour le cache en minutes

22. |

23. | Défaut : 60

24. |

25. */

27. 'timer' => 60

29. );

Il ne reste plus qu'à modifier le template app/views/layout/master.blade.php :

<h1>{{ Config::get('doc.title') }}</h1>

Et le contrôleur :

$nav = Cache::remember('nav', Config::get('doc.timer'), function()

$content = Cache::remember($name, Config::get('doc.timer'), function() use ($name)

Les paramètres sont ainsi centralisés dans un fichier dédié .

XXVIII - Chapitre 28 : Créer une commande

Nous avons vu plusieurs fois l'utilisation d'Artisan au cours des précédents articles, que ce soit pour créer un contrôleur, remplir une base, générer l'autoload, créer un workbench… Pour connaître toutes les commandes disponibles il suffit de taper dans la console php artisan list :

Et pour obtenir le détail d'une commande particulière on utilise la syntaxe php artisan help commande(ou la syntaxe courte php artisan commande -h) :

Mais il se peut que l'on ait besoin d'une commande non prévue, un truc à notre sauce. Laravel nous permet de composer une commande personnalisée facilement, c'est ce que nous allons voir avec un exemple.

Il existe la commande command:make pour créer un paquetage, comme nous l'avons vu dans un chapitre précédentchapitre précédent précédent. Mais il y a un truc un peu agaçant, il faut au préalable renseigner le fichier app/config/workbench avec le nom et l'email. On va créer une commande qui permet de renseigner ces deux valeurs et ensuite lancer la commande de base.

Vous avez la documentation correspondante ici. Je n'ai pas détaillé toutes les possibilités, reportez-vous à cette page pour les détails.

XXVIII-A - Générer la classe

La première chose consiste à générer la classe avec Artisan. La syntaxe est simple :

Le nom de la classe doit comporter le suffixe Command. Voici le code généré :

1. use Illuminate\Console\Command;

2. use Symfony\Component\Console\Input\InputOption;

3. use Symfony\Component\Console\Input\InputArgument;

5. class WorkbenchPlusCommand extends Command {

7. /**

8. * The console command name.

9. *

10. * @var string

11. */

12. protected $name = 'command:name';

14. /**

15. * The console command description.

16. *

17. * @var string

18. */

19. protected $description = 'Command description.';

21. /**

22. * Create a new command instance.

23. *

24. * @return void

25. */

26. public function __construct()

27. {

28. parent::__construct();

29. }

31. /**

32. * Execute the console command.

33. *

34. * @return void

35. */

36. public function fire()

37. {

38. //

39. }

41. /**

42. * Get the console command arguments.

43. *

44. * @return array

45. */

46. protected function getArguments()

47. {

48. return array(

49. array('example', InputArgument::REQUIRED, 'An example argument.'),

50. );

51. }

53. /**

54. * Get the console command options.

55. *

56. * @return array

57. */

58. protected function getOptions()

59. {

60. return array(

61. array('example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null),

62. );

63. }

65. }

Et on enregistre notre commande dans le fichier app/start/artisan.php :

Artisan::add(new WorkbenchPlusCommand);

On va commencer par renseigner le nom de la commande ainsi que sa description :

1. /**

2. * The console command name.

3. *

4. * @var string

5. */

6. protected $name = 'workbenchplus';

8. /**

9. * The console command description.

10. *

11. * @var string

12. */

13. protected $description = 'Create a new package workbench with name and email.';

On vérifie que la commande est bien prise en compte avec php artisan list :

XXVIII-A-1 - Argument et options

Une commande peut avoir un argument et des options. C'est le cas de la commande workbench. On peut le vérifier :

Puisqu'on veut reproduire cette commande et la compléter, il faut prévoir son argument et ses options :

1. /**

2. * Get the console command arguments.

3. *

4. * @return array

5. */

6. protected function getArguments()

7. {

8. return array(

9. array('package', InputArgument::REQUIRED, 'The name (vendor/name) of the package.'),

10. );

11. }

13. /**

14. * Get the console command options.

15. *

16. * @return array

17. */

18. protected function getOptions()

19. {

20. return array(

21. array('resources', null, InputOption::VALUE_NONE, 'Create Laravel specific directories.'),

22. );

23. }

On vérifie :

XXVIII-A-2 - Codage de la commande

Il ne nous reste plus qu'à coder la commande. Il nous faut :

- lire le fichier app/config/workbench.php ;

- interroger l'utilisateur pour avoir les valeurs pour le nom et l'email ;

- modifier le fichier app/config/workbench.php en conséquence ;

- lancer la commande workbench.

Laravel offre la classe Illuminate\Filesystem\Filesystem pour faciliter les manipulations de fichiers. Nous allons donc utiliser cette classe. Voici le code complet de la commande (comme les commentaires de base étaient en anglais j'ai continué dans ce langage ) :

1. use Illuminate\Console\Command;

2. use Symfony\Component\Console\Input\InputOption;

3. use Symfony\Component\Console\Input\InputArgument;

5. class WorkbenchPlusCommand extends Command {

7. /**

8. * The console command name.

9. *

10. * @var string

11. */

12. protected $name = 'workbenchplus';

14. /**

15. * The console command description.

16. *

17. * @var string

18. */

19. protected $description = 'Create a new package workbench.';

21. /**

22. * The file system instance.

23. *

24. * @var string

25. */

26. protected $files;

28. /**

29. * Set configuration item

30. *

31. * @param  string $contents

32. * @param  string $key

33. * @param  string $value

34. * @return void

35. */

36. private function setItem(&$contents, $key, $value)

37. {

38. $pattern = "#('$key' => ').*'#";

39. $replacement = "$1$value'";

40. $contents = preg_replace($pattern, $replacement, $contents);

41. }

42. /**

43. * Create a new command instance.

44. *

45. * @param  \Illuminate\Filesystem\Filesystem  $files

46. * @return void

47. */

48. public function __construct(Filesystem $files)

49. {

50. parent::__construct();

51. $this->files = $files;

52. }

54. /**

55. * Execute the console command.

56. *

57. * @return void

58. */

59. public function fire()

60. {

61. // Name

62. $name = $this->ask("What is the name ?");

63. // Email

64. $email = $this->ask("What is the email ?");

65. // Workbench config file content

66. $contents = $this->files->get($path = $this->laravel['path'].'/config/workbench.php');

67. // Set name

68. $this->setItem($contents, 'name', $name);

69. // Set email

70. $this->setItem($contents, 'email', $email);

71. // Save config file

72. $this->files->put($path, $contents);

73. // Call workbench command

74. $this->call('workbench', array(

75. 'package' => $this->argument('package'),

76. '--resources' => $this->option('resources')

77. ));

78. }

80. /**

81. * Get the console command arguments.

82. *

83. * @return array

84. */

85. protected function getArguments()

86. {

87. return array(

88. array('package', InputArgument::REQUIRED, 'The name (vendor/name) of the package.'),

89. );

90. }

92. /**

93. * Get the console command options.

94. *

95. * @return array

96. */

97. protected function getOptions()

98. {

99. return array(

100. array('resources', null, InputOption::VALUE_OPTIONAL, 'Create Laravel specific directories.', null),

101. );

102. }

104. }

Vous remarquez que j'ai prévu l'injection d'une instance de la classe Filesystem plutôt que de l'utiliser directement dans le code. C'est une approche orientée sur la testabilité du code. Mais du coup il faut compléter l'appel de la commande dans le fichier app/start/artisan.php :

1. use Illuminate\Filesystem\Filesystem;

3. /*

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

5. | Register The Artisan Commands

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

7. |

8. | Each available Artisan command must be registered with the console so

9. | that it is available to be called. We'll register every command so

10. | the console gets access to each of the command object instances.

11. |

12. */

14. Artisan::add(new WorkbenchPlusCommand(new Filesystem));

Et son test :

On vérifie l'écriture des valeurs dans le fichier app/config/workbench.php :

1. return array(

3. /*

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

5. | Workbench Author Name

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

7. |

8. | When you create new packages via the Artisan "workbench" command your

9. | name is needed to generate the composer.json file for your package.

10. | You may specify it now so it is used for all of your workbenches.

11. |

12. */

14. 'name' => 'bestmomo',

16. /*

17. |--------------------------------------------------------------------------

18. | Workbench Author E-Mail Address

19. |--------------------------------------------------------------------------

20. |

21. | Like the option above, your e-mail address is used when generating new

22. | workbench packages. The e-mail is placed in your composer.json file

23. | automatically after the package is created by the workbench tool.

24. |

25. */

27. 'email' => 'bestmomo@chez.moi',

29. );

Et la création du workbench :

On supprime ce dossier et on teste avec l'option -resources :

Résultat :

XXIX - Chapitre 29 : Le routage

XXIX-A - Préambule

Nous avons déjà vu le routage dans un chapitre dédié et ensuite au fil des exemples. Je vous propose maintenant de revenir sur cet aspect important du framework de façon un peu transversale en envisageant toutes les possibilités qu'il nous offre (enfin je vais tenter d'être exhaustif, je compte sur vous pour me signaler mes oublis ).

Pour suivre les exemples, je vous demande de créer un nouvelle installation vierge de Laravel :

Si tout se passe bien vous devez tomber sur le message d'accueil :

Vous savez déjà que les routes se trouvent dans le fichier app/routes.php :

Au départ ce fichier contient peu de code :

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

2. {

3. return View::make('hello');

4. });

Juste de quoi récupérer l'URL http://localhost/laravel/public et créer la vue de l'accueil. Laravel utilise de façon intensive les appels statiques. C'est une façon de rendre la syntaxe simple et élégante. Comparez le code ci-dessus avec celui-ci :

app('router')->get('/', function() {
return app('view')->make('hello');
});

Si vous le testez vous aboutirez au même résultat. Mais avouez que la syntaxe est moins lisible. On peut aussi écrire ce code pour un résultat identique :

$app['router']->get('/', function() use ($app) {
return $app['view']->make('hello');
});

Mais le but de ce fil n'étant pas de discuter la question de l'IOC et des façades, on va se contenter d'utiliser la syntaxe la plus élégante…

XXIX-B - Les verbes

Laravel reconnaît les verbes suivants :

1. Route::get();

2. Route::post();

3. Route::put();

4. Route::delete();

5. Route::any();

Les quatre premiers sont classiques et doivent vous parler, peut-être un peu moins le dernier. C'est une expression « passoire » qui accepte tous les autres. Je vous déconseille son utilisation sauf cas très particulier parce qu'a priori vous savez toujours ce qui vous arrive. On peut donc avoir cette syntaxe fonctionnelle pour notre exemple de base :

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

2. {

3. return View::make('hello');

4. });

XXIX-C - Écriture des routes

On vient de voir la syntaxe élémentaire d'une route, pour laquelle on trouve la description de la route et une fonction anonyme pour effectuer le traitement correspondant. Dans une application on va évidemment se retrouver avec de nombreuses routes. Prenons un exemple simple. Nous voulons répondre aux URL suivants :

- http://localhost/laravel/public/

- http://localhost/laravel/public/login

- http://localhost/laravel/public/page/1

Le code correspondant pour les routes est :

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

2. {

3. return 'accueil';

4. });

6. Route::get('login', function()

7. {

8. return 'login';

9. });

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

12. {

13. return 'page1';

14. });

Le syntaxe est simple. Pour répondre à la route de base on utilise juste '/', et pour les autres routes on désigne le chemin : 'login' ou 'page/1', celui-ci pouvant être constitué de plusieurs éléments.

Notre exemple fonctionne très bien, mais que se passe-t-il si on entre un URL qui n'a pas été prévu ? Faisons l'essai :

Évidemment nous recevons un message d'erreur, pour éviter ce désagrément on va ajouter une gestion sommaire de l'erreur 404 :

1. App::missing(function($exception)

2. {

3. return 'Cette page n\'existe pas !';

4. });

Maintenant quel que soit l'URL reçu il sera efficacement traité.

XXIX-D - Les paramètres

XXIX-D-1 - Présentation

Lorsqu'on utilise le verbe get on a bien souvent des paramètres transmis. À ce moment-là l'URL a une syntaxe variable et ce qu'on a vu précédemment ne suffit plus pour gérer cette situation. Considérez un exemple simple, vous avez un URL pour des articles qui doit comporter le numéro de l'article de cette forme :

http://localhost/laravel/public/article/1

La syntaxe pour la route dans ce cas est la suivante :

1. Route::get('article/{numero}', function($numero)

2. {

3. return 'Article numéro ' . $numero;

4. });

Vous remarquez que le paramètre variable est placé entre accolades. On prévoit aussi un paramètre pour la fonction anonyme avec le même nom, ce qui permet de disposer ensuite de la valeur correspondante.

On n'est pas limité à un paramètre, on peut par exemple répondre à la situation suivante avec deux paramètres : le premier pour la catégorie des articles et le second pour le numéro :

http://localhost/laravel/public/article/20/1

Dans ce cas le code est le suivant :

1. Route::get('article/{categorie}/{numero}', function($categorie, $numero)

2. {

3. return 'Article numéro ' . $numero . ' de la catégorie ' . $categorie;

4. });

XXIX-D-2 - Paramètre optionnel

Un paramètre variable c'est bien, mais parfois on désire également qu'il soit optionnel. Imaginez cette situation : on veut consulter la situation du stock d'articles d'une catégorie complète ou d'un article bien particulier d'une catégorie. En gros donc on veut réagir à ces deux URL :

- http://localhost/laravel/public/stock/20

- http://localhost/laravel/public/stock/20/1

Dans le premier cas on veut un état du stock pour une catégorie d'articles, et dans le second cas pour un article de cette catégorie. On peut évidemment s'en sortir avec deux routes :

1. Route::get('stock/{categorie}', function($categorie)

2. {

3. return 'Stock pour la catégorie ' . $categorie;

4. });

6. Route::get('stock/{categorie}/{article}', function($categorie, $article)

7. {

8. return 'Stock pour l\'article' . $article . ' de la catégorie ' . $categorie;

9. });

Mais on peut aussi tout regrouper dans une seule en rendant le deuxième paramètre optionnel :

1. Route::get('stock/{categorie}/{article?}', function($categorie, $article = null)

2. {

3. if($article) return 'Stock pour l\'article' . $article . ' de la catégorie ' . $categorie;

4. else return 'Stock pour la catégorie ' . $categorie;

5. });

Le fait d'ajouter un point d'interrogation à la fin du nom d'un paramètre le rend optionnel, ce qui est fait ici pour le paramètre article. Il est alors important de prévoir une valeur par défaut, pour le cas où ce paramètre n'est pas renseigné, pour faciliter le codage.

XXIX-D-3 - Contraintes sur les paramètres

On veut aussi parfois limiter les valeurs transmises par un paramètre à une certaine plage. Par exemple dans le premier cas précédent on désire uniquement des valeurs numériques. Il est possible de contraindre les valeurs transmises à l'aide d'une expression régulière :

1. Route::get('article/{numero}', function($numero)

2. {

3. return 'Article numéro ' . $numero;

4. })->where('numero', '[0-9]+');

Dans le cas où vous avez besoin de fixer une contrainte sur plusieurs paramètres vous pouvez passer un tableau :

1. Route::get('stock/{categorie}/{article?}', function($categorie, $article = null)

2. {

3. if($article) return 'Stock pour l\'article' . $article . ' de la catégorie ' . $categorie;

4. else return 'Stock pour la catégorie ' . $categorie;

5. })->where(array('categorie' => '[0-9]+', 'article' => '[0-9]+'));

Si vous avez une contrainte qui se répète sur plusieurs routes il est possible de définir une expression globale pour simplifier le code. Voici un exemple :

1. Route::pattern('id', '[0-9]+');

3. Route::get('stock/categorie/{id}', function($id)

4. {

5. return 'Stock pour la catégorie ' . $id;

6. });

7. Route::get('stock/article/{id}', function($id)

8. {

9. return 'Stock pour l\'article ' . $id;

10. });

XXIX-E - Grouper des routes

Lorsqu'on commence à empiler les routes c'est bien de pouvoir un peu les classer. il est possible de regrouper des routes à partir d'un préfixe identique. Voici un exemple :

1. Route::group(array('prefix' => 'categorie'), function()

2. {

3. Route::get('stock/{id}', function($id)

4. {

5. return "Stock de la catégorie $id";

6. });

7. Route::get('{id_categorie}/{id_article}', function($id_categorie, $id_article)

8. {

9. return "Article numéro $id_article de la catégorie $id_categorie";

10. });

11. });

Ces routes vont intercepter ces deux types d'URL :

- http://localhost/laravel/public/categorie/stock/20

- http://localhost/laravel/public/categorie/10/20

La factorisation du terme « categorie » permet de regrouper les deux routes.

XXIX-F - Les contrôleurs

Vous effectuerez rarement un traitement au niveau du fichier des routes. Vous préférerez organiser votre code dans des contrôleurs. Il faut donc pouvoir au niveau des routes pointer sur des méthodes de contrôleurs. Dans votre installation de base de Laravel vous avez un contrôleur HomeController avec une seule méthode showWelcome. Voyons comment diriger une route sur cette méthode :

Route::get('/', 'HomeController@showWelcome');

Si maintenant vous utilisez l'URL http://localhost/laravel/public vous tombez sur la page d'accueil.

La syntaxe est simple : le nom du contrôleur et celui de sa méthode séparés par l'arobase.

Évidemment si vous avez placé vos contrôleurs dans un espace de nom particulier il faut le signaler. Voici par exemple le contrôleur HomeController maintenant placé dans un espace de nom :

1. <?php

2. namespace ChezMoi;

3. use BaseController;

4. use View;

5. class HomeController extends BaseController {

7. public function showWelcome()

8. {

9. return View::make('hello');

10. }

12. }

Pour pointer ce contrôleur il faut signaler l'espace de nom au niveau de la route :

Route::get('/', 'ChezMoi\HomeController@showWelcome');

XXIX-F-1 - Les contrôleurs RESTful

Un gros avantage de l'utilisation de contrôleurs c'est qu'on peut énormément simplifier le routage en l'automatisant. On peut ainsi créer un contrôleur RESTful, voici un exemple :

1. class ArticleController extends BaseController {

3. public function getIndex()

4. {

5. echo Form::open(array('url' => 'article/ajout'));

6. echo Form::label('nom', 'Entrez le nom : ');

7. echo Form::text('nom');

8. echo Form::submit('Envoyer');

9. echo Form::close();

10. }

12. public function postAjout()

13. {

14. return 'Le nom ' . Input::get('nom') .  ' a été entré.';

15. }

17. }

Ce contrôleur comporte deux méthodes : la première pour afficher un formulaire de saisie, la seconde pour recevoir la soumission du formulaire. Évidemment cet exemple est volontairement minimaliste pour se concentrer sur le routage. Ce qu'il faut remarquer, c'est la syntaxe des noms de méthodes qui suivent la convention de nommage REST. Ces noms sont composés du verbe à utiliser suivi d'un nom. Il faut combiner cela à la définition d'une route pour que ça fonctionne :

Route::controller('article', 'ArticleController');

Maintenant la méthode getIndex du contrôleur correspond à l'URL http://localhost/laravel/public/article/index et le verbe correspondant est get. De la même façon la méthode postAjout correspond à l'URL http://localhost/laravel/public/article/ajout avec le verbe post. Vous voyez que le routage devient extrêmement simple à réaliser.

Il est intéressant de savoir quelles sont les routes vraiment générées, pour cela on peut utiliser la commande routes d'Artisan :

Bon, la mise en page dans ma console n'est pas des plus lisibles mais on distingue quatre routes. Les voici un peu plus clairement exposées :

URI

Action

GET article/index/{one?}/{two?}/{three?}/{four?}/{five?}

ArticleController@getIndex

GET article

ArticleController@getIndex

POST article/ajout/{one?}/{two?}/{three?}/{four?}/{five?}

ArticleController@postAjout

GET article/{_missing}

ArticleController@missingMethod

On trouve non pas deux routes comme on aurait pu s'y attendre mais quatre. On se rend compte qu'on peut accéder à la méthode getIndex avec ces deux URL :

- http://localhost/laravel/public/article

- http://localhost/laravel/public/article/index

C'est une caractéristique de cette méthode d'offrir la possibilité d'y accéder avec l'URL de base.

On se rend compte également qu'il y a cinq paramètres optionnels disponibles, ce qui peut être très utile. Ainsi l'URL suivant sera parfaitement générée sans erreur :

http://localhost/laravel/public/article/index/1/1/1/1/1

Par contre ces paramètres ne sont évidemment pas disponibles avec l'URL de base. On trouve la même possibilité pour la méthode postAjout. En fait ces cinq paramètres optionnels sont générés pour toutes les méthodes. Il faut le savoir.

La dernière route concerne les méthodes manquantes. L'URL de base pointe bien sur le contrôleur, mais aucune méthode ne correspond à l'URI transmis. En effet puisque tous les URL qui commencent par l'URL de base sont dirigés sur le contrôleur, si la méthode associée est manquante on tombe sur une erreur. On doit donc ajouter cette méthode à notre contrôleur :

1. public function missingMethod($parameters = array())

2. {

3. return var_dump($parameters);

4. }

Ici j'ai juste mis en évidence le contenu du paramètre $parameters. Ainsi si on utilise par exemple l'URL http://localhost/laravel/public/article/un/deux/trois, on obtient un tableau avec les paramètres de la requête :

array(3) { [0]=> string(2) "un" [1]=> string(4) "deux" [2]=> string(5) "trois" }

Pour illustrer ces éléments voici un exemple d'utilisation d'un paramètre optionnel :

1. class ArticleController extends BaseController {

3. public function getSaisieArticle($type = 0)

4. {

5. echo Form::open(array('url' => 'article/ajout'));

6. echo Form::label('nom', 'Entrez le nom : ');

7. echo Form::text('nom');

8. if($type == 1) {

9. echo '<br>', Form::label('poids', 'Entrez le poids : ');

10. echo Form::text('poids');

11. }

12. echo '<br>', Form::submit('Envoyer');

13. echo Form::close();

14. }

16. public function postAjout()

17. {

18. $reponse = 'Le nom ' . Input::get('nom') .  ' a été entré.';

19. if (Input::has('poids')) $reponse .= '<br>' . 'Le poids ' . Input::get('poids') .  ' a été entré.';

20. return $reponse;

21. }

23. public function missingMethod($parameters = array())

24. {

25. return 'Je ne reconnais pas cette demande !';

26. }

28. }

Si le paramètre est présent et égal à 1, alors on ajoute un contrôle au formulaire. J'ai aussi montré la possibilité de constituer le nom d'une méthode de plusieurs mots. Il faut en tenir compte dans l'URL. Voici les deux URL correspondants :

- http://localhost/laravel/public/article/saisie-article

- http://localhost/laravel/public/article/saisie-article/1

XXIX-G - Les contrôleurs de ressources

Les contrôleurs RESTful sont intéressants, mais les contrôleurs de ressource le sont encore plus si on a une situation adaptée à leur usage ! J'ai déjà évoqué plusieurs fois cette possibilité dans ce tutoriel, je vais donc surtout évoquer ici l'aspect routage. Pour créer un contrôleur de ressource il suffit d'utiliser Artisan :

Ici j'ai demandé la création d'un contrôleur pour la gestion de livres. Cette simple action a créé le contrôleur avec toutes ses méthodes :

1. class LivreController extends \BaseController {

3. /**

4. * Display a listing of the resource.

5. *

6. * @return Response

7. */

8. public function index()

9. {

10. //

11. }

13. /**

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

15. *

16. * @return Response

17. */

18. public function create()

19. {

20. //

21. }

23. /**

24. * Store a newly created resource in storage.

25. *

26. * @return Response

27. */

28. public function store()

29. {

30. //

31. }

33. /**

34. * Display the specified resource.

35. *

36. * @param  int  $id

37. * @return Response

38. */

39. public function show($id)

40. {

41. //

42. }

44. /**

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

46. *

47. * @param  int  $id

48. * @return Response

49. */

50. public function edit($id)

51. {

52. //

53. }

55. /**

56. * Update the specified resource in storage.

57. *

58. * @param  int  $id

59. * @return Response

60. */

61. public function update($id)

62. {

63. //

64. }

66. /**

67. * Remove the specified resource from storage.

68. *

69. * @param  int  $id

70. * @return Response

71. */

72. public function destroy($id)

73. {

74. //

75. }

77. }

J'ai déjà montré ici comment gérer ce genre de contrôleur. Au niveau du routage il suffit d'une simple ligne :

Route::resource('livres', 'LivreController');

Petite action gros effet ! Voici ce que nous obtenons comme routes :

Cette fois c'est un peu plus lisible… On dispose ainsi de toutes les routes utiles pour gérer du contenu. Parfois on a des particularités qui obligent à ajouter quelques routes, mais pour des situations simples c'est tout à fait adapté et surtout très élégant au niveau du code.

Cette fois nous n'avons pas besoin de gérer les méthodes absentes :en effet étant donné que les méthodes sont standardisées tout autre URL n'est pas dirigé vers le contrôleur, et vous pouvez gérer ça tranquillement au niveau des routes.

Remarquez que chaque route possède par défaut un nom. Si ces noms ne vous conviennent pas vous avez la possibilité de les changer. Par exemple vous n'aimez pas le nom livres.show et vous lui préférez livres.voir. Voici le code correspondant :

Route::resource('livres', 'LivreController',
array('names' => array('show' => 'livres.voir'));

XXIX-H - Ressources imbriquées

Une possibilité pas trop connue concernant les contrôleurs de ressources est celle qui consiste à les imbriquer. Comme un exemple vaut toujours mieux qu'un long discours, imaginez que vous avez des articles rangés dans des catégories. Vous créez un contrôleur de ressource pour ces deux éléments :

Vous créez ainsi les deux contrôleurs. Si vous agissez de façon classique vous créez aussi ces deux routes :

Route::resource('categorie', 'CategorieController');
Route::resource('article', 'ArticleController');

Ce qui vous donne les routes suivantes :

Vous pouvez difficilement avec ces routes gérer par exemple un URL du genre http://localhost/laravel/public/categorie/8/article/5, ce qui serait pourtant bien pratique ! Pour y parvenir on va faire une petite modification dans nos routes :

Route::resource('categorie', 'CategorieController');
Route::resource('categorie.article', 'ArticleController');

Voilà maintenant ce que nous obtenons :

Maintenant les routes sont bien plus intéressantes ! Par exemple pour l'URL considérée ci-dessus on trouve la route GET categorie/{categorie}/article/{article}. Pour la gérer au niveau du contrôleur des articles, il faut évidemment tenir compte du fait qu'on a deux paramètres transmis alors que le générateur n'en a évidemment prévu qu'un seul. Donc par exemple on peut avoir :

1. public function show($id_categorie, $id_article)

2. {

3. return "Voir l'article d'id $id_article de la catégorie $id_categorie";

4. }

Étant donné qu'il y a deux paramètres transmis dans l'URL, on prévoit deux paramètres dans la méthode du contrôleur. Il faut faire la même chose pour les méthodes edit, update et destroy si vous les utilisez.

Vous n'êtes pas limités à un niveau d'imbrication. Par exemple avec ces déclarations :

Route::resource('categorie', 'CategorieController');
Route::resource('categorie.article', 'ArticleController');
Route::resource('categorie.article.composant', 'ComposantController');

On obtient ces routes :

Dans ce cas pour le contrôleur des composants vous aurez évidemment trois paramètres pour les méthodes concernées.

XXIX-I - Contrainte pour les contrôleurs

On a vu qu'il est possible de contraindre les valeurs des paramètres d'une route avec une expression régulière. Est-il possible d'effectuer ce filtrage aussi quand on pointe sur un contrôleur ? A priori oui, mais faisons quand même l'essai. créons pour cela un petit contrôleur :

1. class TestController extends BaseController {

3. public function index($nom)

4. {

5. return "Coucou $nom";

6. }

8. }

Et une route avec une contrainte :

Route::get('/{nom}', 'TestController@index')->where('nom', '[a-z]+');;

Si vous testez ce code, vous verrez que ça fonctionne très bien.

XXIX-J - Redirections et routes nommées

On reçoit des URL du client, mais il arrive aussi souvent qu'on doive effectuer une redirection dans notre code. Là aussi Laravel propose une syntaxe élégante. Il suffit d'utiliser Redirect :

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

2. {

3. return 'Accueil';

4. });

6. Route::get('/test', function()

7. {

8. return Redirect::to('/');

9. });

Ici l'URL http://localhost/laravel/public/test renvoie à la première route. Même si cette syntaxe est pratique elle peut devenir lourde par moments. On a la possibilité de nommer les routes pour simplifier le code :

1. Route::get('/', array('as' => 'accueil', function()

2. {

3. return 'Accueil';

4. }));

6. Route::get('/test', function()

7. {

8. return Redirect::route('accueil');

9. });

Il existe une méthode spéciale si la route se nomme home :

1. Route::get('/', array('as' => 'home', function()

2. {

3. return 'Accueil';

4. }));

6. Route::get('/test', function()

7. {

8. return Redirect::home();

9. });

Mais c'est un peu anecdotique surtout pour nous qui parlons français. Par contre il peut être très utile de transmettre des paramètres lors de la redirection :

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

2. {

3. return Redirect::route('accueil', array('Alfred'));

4. });

6. Route::get('{nom?}', array('as' => 'accueil', function($nom = 'inconnu')

7. {

8. return "Bonjour $nom";

9. }));

- http://localhost/laravel/public/ renvoie « Bonjour inconnu »

- http://localhost/laravel/public/Toto renvoie « Bonjour Toto »

- http://localhost/laravel/public/test renvoie « Bonjour Alfred »

J'en profite pour signaler une chose importante concernant les routes, qui parfois peut faire chercher des heures un bogue. Les routes sont analysées dans l'ordre et dès que l'une d'entre elles correspond à l'URL on ne va pas plus loin. Ici le fait d'inverser les deux routes rendrait le code inopérant parce que l'URL http://localhost/laravel/public/test serait intercepté par la première route avec le message « Bonjour test ».

Le nommage peut aussi s'effectuer pour une route qui pointe une méthode de contrôleur, de même qu'il est possible de rediriger vers un contrôleur. Considérons un contrôleur basique :

class TestController extends BaseController {
public function index(){return "Informations";}
}

Et maintenant prévoyons une route nommée et deux redirections possibles avec deux syntaxes :

1. Route::get('infos', array(

2. 'as' => 'informations',

3. 'uses' => 'TestController@index'

4. ));

6. Route::get('test1', function()

7. {

8. return Redirect::action('TestController@index');

9. });

11. Route::get('test2', function()

12. {

13. return Redirect::route('informations');

14. });

- http://localhost/laravel/public/infos   renvoie « Informations »

- http://localhost/laravel/public/test1   renvoie « Informations »

- http://localhost/laravel/public/test2   renvoie « Informations »

Il est aussi possible de passer des paramètres pour une redirection vers un contrôleur. Ajoutons un paramètre à notre contrôleur d'exemple :

class TestController extends BaseController {
public function index($sujet)   {   return "Informations sur $sujet";   }
}

Et voici les routes :

1. Route::get('infos/{sujet}', array(

2. 'as' => 'informations',

3. 'uses' => 'TestController@index'

4. ));

6. Route::get('test1', function()

7. {

8. return Redirect::action('TestController@index', array('sujet' => 'la course'));

9. });

11. Route::get('test2', function()

12. {

13. return Redirect::route('informations', array('la chasse'));

14. });

- http://localhost/laravel/public/infos/alimentation renvoie « Informations sur alimentation »

- http://localhost/laravel/public/test1 renvoie « Informations sur la course »

- http://localhost/laravel/public/test2 renvoie « Informations sur la chasse »

Remarquez les deux syntaxes pour transmettre le paramètre. L'utilisation de paramètres nommés se justifie s'il y en a plusieurs à transmettre.

On peut aussi transmettre des informations par la session. Modifions le contrôleur :

1. class TestController extends BaseController {

2. public function index() {

3. if (Session::has('message'))    return "Informations sur " . Session::get('message');

4. else return "Pas d'information";

5. }

6. }

Et les routes :

1. Route::get('infos', array('uses' => 'TestController@index'));

3. Route::get('test', function()

4. {

5. return Redirect::action('TestController@index')->with('message', 'la chasse');

6. });

- http://localhost/laravel/public/infos renvoie « Pas d'information »

- http://localhost/laravel/public/test renvoie « Informations sur la chasse »

XXIX-K - Les méthodes back et refresh

Il arrive fréquemment qu'on doive revenir à l'URL précédent. Dans ce cas il est évidemment possible de désigner explicitement la route mais on peut également utiliser la méthode bien pratique back. Voici un exemple élémentaire de scénario avec un formulaire pour entrer un nom, une validation, et un retour au formulaire, avec affichage d'une information en cas d'erreur :

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

2. {

3. if(Session::has('info')) echo 'Vous avez fait une erreur de saisie !', '<br>';

4. echo Form::open(array('url' => 'ajout'));

5. echo Form::label('nom', 'Entrez le nom : ');

6. echo Form::text('nom');

7. echo Form::submit('Envoyer');

8. echo Form::close();

9. });

11. Route::post('ajout', array('before' => 'csrf', function()

12. {

13. $validator = Validator::make(Input::all(), array('nom' => 'required|min:5'));

14. if ($validator->fails()) return Redirect::back()->with('info', 'Erreur !');

15. else return 'Coucou ' . Input::get('nom');

16. }));

La ligne intéressante est celle qui concerne la redirection :

if ($validator->fails()) return Redirect::back()->with('info', 'Erreur !');

Avec cette méthode on revient naturellement afficher le formulaire en cas d'erreur de saisie.

Voyons maintenant une autre approche avec ce code, en apportant un petit changement afin d'avoir le même URL pour la soumission du formulaire et son traitement :

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

2. {

3. if(Session::has('info')) echo 'Vous avez fait une erreur de saisie !', '<br>';

4. echo Form::open(array('url' => 'ajout'));

5. echo Form::label('nom', 'Entrez le nom : ');

6. echo Form::text('nom');

7. echo Form::submit('Envoyer');

8. echo Form::close();

9. });

11. Route::post('ajout', array('before' => 'csrf', function()

12. {

13. $validator = Validator::make(Input::all(), array('nom' => 'required|min:5'));

14. if ($validator->fails()) return Redirect::refresh()->with('info', 'Erreur !');

15. else return 'Coucou ' . Input::get('nom');

16. }));

Le fonctionnement est le même, mais on a juste l'URL http://localhost/laravel/public/ajout avec le verbe « get » à la présentation du formulaire, et « post » à la soumission. Cette fois j'ai utilisé la méthode refresh pour rediriger sur le même URL en cas d'erreur :

if ($validator->fails()) return Redirect::refresh()->with('info', 'Erreur !');

J'aurais évidemment pu encore utiliser back, mais il fallait bien que je trouve un cas pour refresh qui est quand même d'une utilisation moins courante .

XXIX-L - Injection d'un modèle

Si vous avez besoin d'informations issues d'une base données et que vous utilisez Eloquent, vous pouvez injecter un modèle dans une vue. Par défaut vous avez le modèle User dans l'installation de base de Laravel. Il suffit de créer une base et d'ajouter des enregistrements pour tester ça. Ensuite le codage est simple :

1. Route::model('user', 'User');

3. Route::get('{user}', function(User $user)

4. {

5. return $user->username;

6. });

Avec l'URL http://localhost/laravel/public/1 on récupère l'enregistrement qui a l'ID 1 comme paramètre de la fonction anonyme de la route. On peut ensuite utiliser ce paramètre comme on veut. ici je me contente d'afficher le nom de l'utilisateur.

Que se passe-t-il si cet ID ne correspond à aucun enregistrement ? La route est alors ignorée et on se retrouve avec une erreur 404 qu'il suffit de gérer comme les autres. Si ce comportement ne vous convient pas, il est possible de gérer spécifiquement l'erreur. C'est ce qui est indiqué dans la documentation avec ce code :

1. Route::model('user', 'User', function()

2. {

3. throw new NotFoundException;

4. });

Quand on l'utilise, on tombe sur cette erreur :

On dirait que cette classe NotFoundException n'est pas très connue . Bon on va arranger ça. Il faut déclarer cette classe, par exemple dans un fichier app/exception.php :

use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class NotFoundException extends NotFoundHttpException {}

Et il faut aussi informer Composer :

"classmap": [
"app/exceptions.php",
"app/commands",

Et faire un petit coup de dumpautoload. Ensuite on écrit le code pour gérer cette exception, le mieux étant dans le fichier app/start/global.php :

1. App::error(function(NotFoundException $exception)

2. {

3. return 'Je ne connais pas cet id !';

4. });

Maintenant ça fonctionne .

Une dernière possibilité est de créer une requête spécifique :

1. Route::bind('user', function($id, $route)

2. {

3. return User::find($id);

4. });

6. Route::get('{user}', function(User $user)

7. {

8. return $user->username;

9. });

Bon là je me suis contenté de trouver l'utilisateur avec l'ID passé. Mais sur ce principe on peut concevoir des recherches élaborées.

Dans un prochain chapitre je développerai la question du filtrage qui est évidemment intimement lié au routage .

XXX - Chapitre 30 : Le filtrage

On a vu dans le chapitre précédent qu'on peut contraindre les paramètres d'une route, mais qu'on est rapidement assez limité. Si on désire quelque chose de plus complet et efficace, on a à notre disposition le filtrage. Les filtres sont des règles d'action que l'on applique sur les routes, en amont (before) ou en aval (after). Ils se situent physiquement dans le dossier app/filters.php :

XXX-A - Les filtres par défaut

Par défaut on y trouve déjà les filtres before, after, auth, auth.basic, guest et csrf :

1. App::before(function($request)

2. {

3. //

4. });

6. App::after(function($request, $response)

7. {

8. //

9. });

11. Route::filter('auth', function()

12. {

13. if (Auth::guest()) return Redirect::guest('login');

14. });

16. Route::filter('auth.basic', function()

17. {

18. return Auth::basic();

19. });

21. Route::filter('guest', function()

22. {

23. if (Auth::check()) return Redirect::to('/');

24. });

26. Route::filter('csrf', function()

27. {

28. if (Session::token() != Input::get('_token'))

29. {

30. throw new Illuminate\Session\TokenMismatchException;

31. }

32. });

XXX-A-1 - Les filtres globaux

Les filtres before et after sont particuliers parce qu'ils s'appliquent à toutes les routes. Autrement dit le filtrage que vous effectuez à ce niveau concernera tous les URL qui arrivent. Imaginez par exemple que vous voulez appliquer le filtre CSRF à toutes les requêtes de type « post ». Vous pouvez évidemment le prévoir dans vos routes ou vos contrôleurs, mais vous pouvez aussi anticiper dans le filtre before, par exemple ainsi :

1. App::before(function($request)

2. {

3. if ($request->getMethod() === 'POST') Route::callRouteFilter('csrf', [], '', $request);

4. });

De la même façon vous pouvez intervenir sur la réponse avec le filtre after. Supposons que vous voulez ajouter quelque chose systématiquement, vous pouvez le faire facilement :

1. App::after(

2. function ($request, $response) {

3. $content = $response->getContent() . '<br>Mon petit ajout';

4. $response->setContent($content);

5. }

6. );

Maintenant avec la route :

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

et l'URL http://localhost/laravel/public/ vous obtenez :

Coucou
Mon petit ajout

À l'usage on utilise essentiellement les filtres before. En effet il est intéressant d'effectuer un tri avant la gestion des routes, la faire après présente bien moins d'intérêt et ne correspond qu'à des cas très particuliers.

XXX-B - Les filtres d'authentification

Il y a trois filtres prévus de base pour l'authentification :

- auth pour vérifier si l'utilisateur est authentifié ;

- auth.basic pour vérifier l'authentification « basique » qui utilise l'email;

- guest qui est l'inverse de auth.

Pour utiliser ces filtres sur une route la syntaxe est la suivante :

1. Route::get('messages', array('before' => 'auth', function()

2. {

3. echo 'Je peux lire les messages !';

4. }));

Ici on applique le filtre auth sur la route en effectuant ce filtrage avant d'accéder à la route (before), donc si l'utilisateur n'est pas authentifié il n'aura pas accès aux messages. Si vous regardez l'URL généré dans ce cas vous trouvez http://localhost/laravel/public/login. Pourquoi ? Parce que dans le filtre auth on a une redirection en cas d'échec :

if (Auth::guest()) return Redirect::guest('login');

Il faut donc gérer cette redirection dans le cadre d'une application, comme je l'ai montré dans ce chapitre chapitrechapitre. Vous pouvez évidemment modifier la cible si « login » ne vous plaît pas. Ppour tester l'efficacité du filtrage dans sa version positive vous pouvez authentifier artificiellement un utilisateur :

Auth::login(User::find(1));

Il faut évidemment avoir une table active dans votre base de données et le modèle correspondant. Et cette fois vous devez obtenir :

Je peux lire les messages !

Si vous voulez activer une route, non plus pour les utilisateurs authentifiés mais pour les autres, alors il faut utiliser le filtre guest :

1. Route::get('messages', array('before' => 'guest', function()

2. {

3. echo 'Je peux lire les messages !';

4. }));

Si vous voulez tester tout ça pensez à supprimer les cookies entre deux tests .

Le filtre auth.basic s'utilise exactement de la même façon. Il est basé sur la vérification de l'email par défaut mais vous pouvez changer ça avec ce code :

Auth::basic('username');

Maintenant c'est le nom de l'utilisateur qui va servir de référence, et on obtient une fenêtre de connexion :

XXX-C - Le filtre CSRF

Ce filtre sert à se protéger contre les attaques de type CSRF. Il est appliqué systématiquement pour les formulaires, mais vous devez préciser que vous l'utilisez au niveau de la requête de retour. Il y a plusieurs façons de localiser ce filtre. On a déjà vu ci-dessus une façon de l'appliquer systématiquement à toutes les requêtes « post » qui arrivent dans le filtre global before. Ce n'est pas toujours un comportement judicieux. On peut l'appliquer directement à une route :

1. Route::post('ajout', array('before' => 'csrf', function()

2. {

3. return 'Le token est le bon !'

4. }));

Si vous avez plusieurs routes qui nécessitent ce filtre vous pouvez les grouper :

1. Route::group(array('before' => 'csrf'), function()

2. {

3. Route::post('ajout', function()

4. {

5. ...

6. });

8. Route::post('edition', function()

9. {

10. ...

11. });

12. });

Une autre façon de faire est de prévoir le filtrage au niveau d'un contrôleur :

1. public function __construct()

2. {

3. $this->beforeFilter('csrf', array('on' => 'post'));

4. }

Si vous le faites dans le BaseController il concernera évidemment tous les contrôleurs !

XXX-D - Créer un filtre

On peut aussi ajouter ses propres filtres dans ce fichier. C'est ce que nous allons faire maintenant. Créer un filtre est tout simple. Admettons que l'on veuille limiter des noms à une liste, voilà le filtre :

1. Route::filter('noms', function()

2. {

3. $noms = array('Alfred','Gaston');

4. if (!in_array(Route::input('nom'), $noms)) return Redirect::to('/');

5. });

On utilise la méthode filter pour créer un filtre. Comme les filtres vus précédemment, il a un nom, ici noms. Il a également une fonction anonyme pour le traitement. Ici on a un tableau avec les noms autorisés. On teste que le nom est dans le tableau et, si ce n'est pas le cas, on redirige vers la page d'accueil. Par contre si c'est le cas on traite la route de façon classique en continuant l'exécution normale. Voici les routes :

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

2. {

3. return 'Accueil';

4. });

5. Route::get('{nom}', array('before' => 'noms', function($nom)

6. {

7. return "Salut $nom";

8. }));

La syntaxe pour intégrer un filtre à une route est simple comme vous pouvez le voir. Au niveau du fonctionnement on a :

- http://localhost/laravel/public/  renvoie « Accueil » ;

- http://localhost/laravel/public/Gaston  renvoie « Salut Gaston » ;

- http://localhost/laravel/public/Paul  renvoie « Accueil ».

Vous pouvez utiliser after au lieu de before à ce niveau, mais alors ce n'est plus vraiment un filtre de route et je n'en vois pas trop l'utilité .

XXX-E - Plusieurs filtres

On peut appliquer autant de filtres que l'on veut. Par exemple complétons le cas précédent en prévoyant aussi un âge limite :

1. Route::filter('ages', function()

2. {

3. if (Route::input('age') < 20) return Redirect::to('/');

4. });

On applique les deux filtres à la route :

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

2. {

3. return 'Accueil';

4. });

5. Route::get('{nom}/{age}', array('before' => 'noms|ages', function($nom, $age)

6. {

7. return "Salut $nom, vous avez $age ans";

8. }));

On a mis les noms des deux filtres séparés par le signe « | ». Les filtres sont exécutés l'un après l'autre en commençant par la gauche. Cet ordre d'exécution peut avoir une incidence dans des cas particuliers, mais n'est en général pas important comme dans notre exemple. Le fonctionnement est le suivant :

- http://localhost/laravel/public/  renvoie « Accueil » ;

- http://localhost/laravel/public/Gaston/25  renvoie « Salut Gaston, vous avez 25 ans » ;

- http://localhost/laravel/public/Gaston/5  renvoie « Accueil » ;

- http://localhost/laravel/public/Paul/50  renvoie « Accueil ».

Rien ne vous empêche de coupler un filtre « before » avec un filtre « after ». La syntaxe est alors la suivante :

1. Route::get('{nom}/{age}', array(

2. 'before' => 'noms',

3. 'after' => 'ages',

4. function($nom, $age) {

5. return "Salut $nom, vous avez $age ans";

6. }));

XXX-F - Paramètres dans les filtres

XXX-F-1 - Un paramètre

Il peut être utile de transmettre des paramètres à un filtre. Supposons que notre filtre pour l'âge doive être utilisé plusieurs fois avec des âges limites différents. À ce moment ce serait bien que la route envoie cette limite au filtre. Voici notre exemple modifié :

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

2. {

3. return 'Accueil';

4. });

5. Route::get('cas1/{age}', array('before' => 'ages:20', function($age)

6. {

7. return "Salut vous avez $age ans";

8. }));

9. Route::get('cas2/{age}', array('before' => 'ages:40', function($age)

10. {

11. return "Salut vous avez $age ans";

12. }));

Et voici le filtre :

1. Route::filter('ages', function($route, $request, $age)

2. {

3. if (Route::input('age') < $age) return Redirect::to('/');

4. });

Remarquez les paramètres du filtre : $route qui est la route actuelle, $request qui est la requête en cours, puis notre paramètre. Voyons si ça fonctionne :

- http://localhost/laravel/public/  renvoie « Accueil » ;

- http://localhost/laravel/public/cas1/25  renvoie « Salut vous avez 25 ans » ;

- http://localhost/laravel/public/cas1/5  renvoie « Accueil » ;

- http://localhost/laravel/public/cas2/45  renvoie « Salut vous avez 45 ans » ;

- http://localhost/laravel/public/cas2/25  renvoie « Accueil ».

Aurait-on la même syntaxe pour un filtre « after » ? Pas tout à fait parce qu'un filtre « after » est appliqué après traitement de la route, on a donc généré une réponse et il est sans doute utile de disposer de cette réponse dans le code du filtre. C'est pour cette raison que dans le cas d'un filtre « after » on a un paramètre supplémentaire pour pouvoir agir sur la réponse :

1. Route::filter('ages', function($route, $request, $response, $mon_paramètre)

2. {

3. ....

4. });

XXX-F-2 - Plusieurs paramètres

Complétons notre exemple avec un deuxième paramètre :

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

2. {

3. return 'Accueil des membres';

4. });

5. Route::get('actifs', function()

6. {

7. return 'Accueil des actifs';

8. });

9. Route::get('cas1/{age}', array('before' => 'ages:membres,20', function($age)

10. {

11. return "Salut vous avez $age ans";

12. }));

13. Route::get('cas2/{age}', array('before' => 'ages:actifs,40', function($age)

14. {

15. return "Salut vous avez $age ans";

16. }));

Et voici le filtre :

1. Route::filter('ages', function($route, $request, $statut, $age)

2. {

3. if (Route::input('age') < $age) return Redirect::to($statut);

4. });

- http://localhost/laravel/public/cas1/25  renvoie « Salut vous avez 25 ans » ;

- http://localhost/laravel/public/cas1/5  renvoie « Accueil des membres » ;

- http://localhost/laravel/public/cas2/45  renvoie « Salut vous avez 45 ans » ;

- http://localhost/laravel/public/cas2/25  renvoie « Accueil des actifs ».

XXX-G - Un patron de route

Il peut arriver que vous deviez faire agir un filtre sur plusieurs routes qui ont la même structure d'URL. Par exemple imaginez une gestion d'articles avec tous les URL qui commencent par « articles/ ». Et vous désirez que ces URL ne soient accessibles que pour les utilisateurs authentifiés. Vous pouvez alors utiliser cette syntaxe :

1. Route::when('articles/*', 'articles');

2. Route::get('articles/ajout', function()

3. {

4. return "Ajout d'un article";

5. });

6. Route::get('articles/suppression/{id}', function($id)

7. {

8. return "Suppression de l'article d'id $id";

9. });

Et voici un exemple rudimentaire pour le filtre :

1. Route::filter('articles', function()

2. {

3. if(!Auth::check()) return 'Vous devez être connecté pour agir sur les articles !';

4. });

Si vous voulez authentifier de façon artificielle un utilisateur pour tester ce genre de code, vous pouvez utiliser ce code :

Auth::login(User::find(1));

Mais ça ne fonctionnera que si vous avez une base renseignée et accessible .

XXX-H - Les classes de filtres

Mettre le code d'un filtre dans une fonction anonyme est une stratégie efficace et en général largement suffisante. Mais il se peut que cela ne vous suffise pas. Par principe une fonction anonyme est localisée, c'est même son principal intérêt, mais elle peut se retourner contre vous selon vos besoins. Si vous voulez une approche plus souple alors vous pouvez créer une classe pour votre filtre et instancier cette classe où et quand vous voulez.

Ajouter une classe à Laravel implique d'informer L'IoC sinon elle restera inconnue et donc inutilisable. Pour gérer convenablement votre code il faut bien la classer et la localiser. Si vous avez des classes de filtres, autant créer un dossier pour mettre ces classes, par exemple app/filters. Vous devez alors informer Composer :

1. "autoload": {

2. "classmap": [

3. "app/filters",

4. "app/commands",

5. "app/controllers",

6. "app/models",

7. "app/database/migrations",

8. "app/database/seeds",

9. "app/tests/TestCase.php"

10. ]

11. },

Et évidemment faire un dumpautoload. Vous avez ainsi un endroit judicieux pour placer vos classes de filtres.

Reprenons notre exemple de filtre paramétré sur l'âge vu précédemment, et créons une classe pour ce filtre (app/filters/AgeFilter.php) :

1. class AgeFilter

2. {

3. public function filter($route, $request, $age)

4. {

5. if (Route::input('age') < $age) return 'Vous êtes trop jeune !';

6. }

7. }

Il faut ensuite agir sur la route :

1. Route::filter('ages', 'AgeFilter');

2. Route::get('{age}', array('before' => 'ages:20', function($age) {

3. return "Vous avez $age ans";

4. }));

Mis à part l'organisation du code, tout le reste est conforme à ce que nous avons vu pour les fonctions anonymes.

XXX-I - Filtres sur les contrôleurs

Pour le moment nous avons vu les filtres appliqués sur les routes, voyons à présent comment faire avec des contrôleurs.

XXX-I-1 - Filtre déclaré dans la route

On peut évidemment encore spécifier le filtre au niveau d'une route. Voilà un contrôleur :

1. class TestController extends BaseController {

2. public function index($nom) {

3. return "Bonjour $nom";

4. }

5. }

On reprend le filtre pour les noms vu précédemment :

1. Route::filter('noms', function()

2. {

3. $noms = array('Alfred','Gaston');

4. if (!in_array(Route::input('nom'), $noms)) return Redirect::to('/');

5. });

On spécifie le filtre au niveau de la route :

1. Route::get('{nom}', array(

2. 'before' => 'noms',

3. 'uses' => 'testController@index'

4. ));

On a le fonctionnement :

- http://localhost/laravel/public/Gaston  renvoie « Bonjour Gaston » ;

- http://localhost/laravel/public/Paul  renvoie « Accueil » si on a prévu la route pour l'accueil.

XXX-I-2 - Filtre déclaré dans le contrôleur

Plutôt que dans la route, c'est plus élégant de spécifier le filtre dans le contrôleur :

1. class TestController extends BaseController {

2. public function __construct()   {

3. $this->beforeFilter('noms');

4. }

5. public function index($nom) {

6. return "Bonjour $nom";

7. }

8. }

À ce moment-là la route se contente de pointer la méthode du contrôleur :

Route::get('{nom}', array('uses' => 'testController@index'));

Tout fonctionne bien mais… si on déclare ainsi ce filtre il sera appliqué à toutes les méthodes du contrôleur. Ce n'est pas toujours ce qu'on désire. Heureusement, on a la possibilité de spécifier les méthodes pour lesquelles le filtre doit agir. Complétons notre contrôleur :

1. class TestController extends BaseController {

2. public function __construct()   {

3. $this->beforeFilter('noms', array('only' => 'index'));

4. }

5. public function index($nom) {

6. return "Bonjour $nom";

7. }

8. public function login($nom) {

9. return "Login de $nom";

10. }

11. public function logout($nom)    {

12. return "Logout de $nom";

13. }

14. }

J'ai ajouté deux méthodes, et aussi complété la déclaration du filtre, pour cibler uniquement la méthode index. Voici les routes associées :

Route::get('index/{nom}', array('uses' => 'testController@index'));
Route::get('login/{nom}', array('uses' => 'testController@login'));
Route::get('logout/{nom}', array('uses' => 'testController@logout'));

Et le fonctionnement :

- http://localhost/laravel/public/index/Gaston  renvoie « Bonjour Gaston » ;

- http://localhost/laravel/public/index/Paul  renvoie « Accueil » ;

- http://localhost/laravel/public/login/Paul  renvoie « Login de Paul » ;

- http://localhost/laravel/public/logout/Paul  renvoie « Logout de Paul ».

On se rend compte que le filtre n'est effectivement appliqué qu'à la méthode index. On peut coder avec la logique inverse en excluant les méthodes non concernées :

$this->beforeFilter('noms', array('except' => array('login', 'logout')));

À vous de choisir .

XXX-I-3 - Filtre dans le contrôleur

Vous pouvez aussi mettre le code du filtre directement dans le contrôleur :

1. public function __construct()   {

2. $this->beforeFilter(function() {

3. $noms = array('Alfred','Gaston');

4. if (!in_array(Route::input('nom'), $noms)) return "Le nom n'est pas correct";

5. }, array('only' => 'index'));

6. }

C'est une autre façon d'organiser son code si le filtre ne concerne effectivement que ce contrôleur. Ici le code est placé dans le constructeur. Vous pouvez également le mettre dans une méthode du contrôleur :

1. public function __construct()

2. {

3. $this->beforeFilter('@filterAges', array('only' => 'index'));

4. }

5. public function filterAges($route, $request)

6. {

7. $noms = array('Alfred','Gaston');

8. if (!in_array(Route::input('nom'), $noms)) return "Le nom n'est pas correct";

9. }

Il suffit de mettre le nom de la méthode précédé du signe « @ » comme premier paramètre de la méthode beforeFilter.

XXX-I-4 - Filtres paramétrés

On peut maintenant se poser une question : comment transmettre un paramètre à un filtre déclaré ou codé dans un contrôleur ? Prenons encore notre filtre pour l'âge :

1. Route::filter('ages', function($route, $request, $statut, $age)

2. {

3. if (Route::input('age') < $age) return 'Vous êtes trop jeune !';

4. });

Ce filtre attend un paramètre comme limite d'âge. Au niveau de la route on appelle de façon classique la méthode du contrôleur :

Route::get('{age}', array('uses' => 'testController@index'));

Et dans le contrôleur on déclare le filtre avec son paramètre :

1. public function __construct()

2. {

3. $this->beforeFilter('ages:20', array('only' => 'index'));

4. }

5. public function index($age) {

6. return "Vous avez $age ans.";

7. }

Tout cela est simple mais imaginez maintenant que vous avez un contrôleur de ressources, par exemple pour des livres que vous référencez ainsi au niveau de la route :

Route::resource('livre', 'LivreController');

Vous avez dans le contrôleur une méthode show($id). Si vous voulez filtrer cet ID comment faire ? Vous pouvez récupérer le paramètre dans le filtre avec la méthode getParameter :

1. Route::filter('livres', function($route, $request)

2. {

3. $id = $route->getParameter('livre');

4. if($id > 10) return "Cet id de livre n'est pas correct";

5. });

Le codage dans le contrôleur est classique :

1. public function __construct() {

2. $this->beforeFilter('livres', array('only' => array('show')));

3. }

4. public function show($id)

5. {

6. return "Affichage du livre avec l'id $id";

7. }

Je pense que mon tour d'horizon du filtrage est relativement complet, si vous voyez des lacunes dites-le moi .

XXXI - Chapitre 31 : Le conteneur de dépendances (IoC)

XXXI-A - L'objet Application

Le conteneur de dépendances de Laravel constitue la clé de fonctionnement du framework. On peut très bien utiliser Laravel sans jamais avoir regardé dans la « salle des machines », mais il devient indispensable de s'y intéresser dès qu'on veut ajouter des fonctionnalités. Alors en quoi consiste ce conteneur ? Comme son nom l'indique, c'est une « boîte » dans laquelle on va mettre tout ce qui nous est utile. Déjà il porte un nom : c'est l'objet Application. Voici le cycle de démarrage de Laravel :

L'objet Application sert de conteneur pour Laravel. Si on regarde la signature de la classe :

class Application extends Container implements HttpKernelInterface, TerminableInterface, ResponsePreparerInterface

On se rend compte qu'elle étend la classe Container. Regardons celle-ci :

class Container implements ArrayAccess

Cette classe au nom explicite implémente l'interface ArrayAccess qui est une interface standard de PHP :

1. ArrayAccess {

2. /* Méthodes */

3. abstract public boolean offsetExists ( mixed $offset )

4. abstract public mixed offsetGet ( mixed $offset )

5. abstract public void offsetSet ( mixed $offset , mixed $value )

6. abstract public void offsetUnset ( mixed $offset )

7. }

Ce qui veut dire qu'on peut accéder au contenu de l'application comme s'il s'agissait d'un tableau. La classe Container évidemment définit les quatre méthodes :

1. /**

2. * Determine if a given offset exists.

3. *

4. * @param  string  $key

5. * @return bool

6. */

7. public function offsetExists($key)

8. {

9. return isset($this->bindings[$key]);

10. }

12. /**

13. * Get the value at a given offset.

14. *

15. * @param  string  $key

16. * @return mixed

17. */

18. public function offsetGet($key)

19. {

20. return $this->make($key);

21. }

23. /**

24. * Set the value at a given offset.

25. *

26. * @param  string  $key

27. * @param  mixed   $value

28. * @return void

29. */

30. public function offsetSet($key, $value)

31. {

32. // If the value is not a Closure, we will make it one. This simply gives

33. // more "drop-in" replacement functionality for the Pimple which this

34. // container's simplest functions are base modeled and built after.

35. if ( ! $value instanceof Closure)

36. {

37. $value = function() use ($value)

38. {

39. return $value;

40. };

41. }

43. $this->bind($key, $value);

44. }

46. /**

47. * Unset the value at a given offset.

48. *

49. * @param  string  $key

50. * @return void

51. */

52. public function offsetUnset($key)

53. {

54. unset($this->bindings[$key]);

56. unset($this->instances[$key]);

57. }

On peut se demander maintenant comment on accède à cet objet Application. Quand on regarde la documentation on ne trouve que des appels statiques du genre :

Route::get

C'est le système des façades utilisé par Laravel qui permet ce genre d'appel. Mais comment avoir une référence du conteneur si on en a besoin ? Il faut utiliser la méthode getFacadeRoot. Dans le fichier des routes entrez ce code :

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

2. {

3. $app = App::getFacadeRoot();

4. dd($app);

5. });

Vous verrez apparaître le contenu de l'objet Application, il est très fourni .

XXXI-B - Gérer du contenu

Puisqu'on a un conteneur et qu'on peut y accéder comme un tableau, voyons comment utiliser cette possibilité :

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

2. {

3. $app = App::getFacadeRoot();

4. $app['nom'] = 'Toto';

5. echo $app['nom'];

6. });

Si tout va bien on voit apparaître « Toto » comme résultat. On a donc mis une valeur textuelle dans le conteneur et on a pu la récupérer. On obtient le même résultat avec ce code :

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

2. {

3. $app = App::getFacadeRoot();

4. $app->offsetSet('nom', 'Toto');

5. echo $app->offsetGet('nom');

6. });

On a juste appelé directement les méthodes de la classe Container.

On peut mettre ce que l'on veut dans le conteneur. Voici le code enrichi pour montrer que ça fonctionne pour des tableaux, des objets et des closures (fermetures) :

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

2. {

3. $app = App::getFacadeRoot();

5. class MaClasse { public $propriété = 'je suis une propriété'; };

6. $app['nom'] = 'Toto';

7. $app['tableau'] = Array('un' => 1, 'deux' => 2);

8. $app['objet'] = new MaClasse;

9. $app['closure'] = function() { return 'Je suis une closure !'; };

11. echo $app['nom'], '<br>';

12. echo var_dump($app['tableau']), '<br>';

13. echo var_dump($app['objet']), '<br>';

14. echo $app['closure'];

15. });

On obtient ce résultat :

1. Toto

2. array(2) { ["un"]=> int(1) ["deux"]=> int(2) }

3. object(MaClasse)#224 (1) { ["propriété"]=> string(23) "je suis une propriété" }

4. Je suis une closure !

Si vous regardez de plus près le code de la méthode offsetSet, vous allez voir qu'on transforme en fait toutes les valeurs en closures :

1. public function offsetSet($key, $value)

2. {

3. // If the value is not a Closure, we will make it one. This simply gives

4. // more "drop-in" replacement functionality for the Pimple which this

5. // container's simplest functions are base modeled and built after.

6. if ( ! $value instanceof Closure)

7. {

8. $value = function() use ($value)

9. {

10. return $value;

11. };

12. }

14. $this->bind($key, $value);

15. }

On va comprendre un peu plus loin pourquoi il doit en être ainsi…

XXXI-C - Liaison avec le conteneur

On va pousser un peu plus loin la compréhension du conteneur avec la liaison. Regardons cette ligne de code de la méthode offsetSet, déjà vue ci-dessus :

$this->bind($key, $value);

C'est cette méthode bind qui va établir la liaison avec ce qu'on veut mettre dans le conteneur. Voici la signature de cette méthode :

public function bind($abstract, $concrete = null, $shared = false)

On a trois paramètres dont les deux derniers sont optionnels. Faisons un essai :

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

2. {

3. $app = App::getFacadeRoot();

4. $app->bind('closure', function() { return 'Je suis une closure !'; });

5. echo $app['closure'];

6. });

On se rend compte que ça fonctionne, on a bien mis la closure dans le conteneur en lui transmettant un identifiant et une valeur.

La méthode make est l'inverse de bind. Elle permet de récupérer ce qu'on a mis dans le conteneur :

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

2. {

3. $app = App::getFacadeRoot();

4. $app->bind('closure', function() { return 'Je suis une closure !'; });

5. echo $app->make('closure');

6. });

Si on utilise la façade on rend le code plus simple à lire :

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

2. {

3. App::bind('closure', function() { return 'Je suis une closure !'; });

4. echo App::make('closure');

5. });

XXXI-D - Liaison de classes et injection de dépendance

C'est bien de pouvoir mettre dans le conteneur des valeurs, des fonctions anonymes, des objets… mais c'est beaucoup plus intéressant d'y placer des classes. Mais il s'agit là d'un abus de langage, une classe n'est pas une entité mais juste un moule pour des objets. On va alors parler plutôt de dépendance. Regardez cet exemple :

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

2. {

3. class Expression {

4. public function coucou()    {

5. return 'Coucou toi !';

6. }

7. public function dire($texte)    {

8. return 'Je dis ' . $texte;

9. }

10. };

12. App::bind('expression', function()

13. {

14. return new Expression;

15. });

17. $expression =  App::make('expression');

18. echo $expression->coucou(), '<br>', $expression->dire('que je vais bien');

19. });

Avec comme résultat :

Coucou toi !
Je dis que je vais bien

Je définis une classe Expression avec deux méthodes. Ensuite la classe est référencée dans le conteneur avec la méthode bind avec le nom expression et une fonction anonyme, celle-ci se contentant de créer un objet à partir de la classe. Lorsque je veux me servir de ma classe j'utilise la méthode make. Quel est l'avantage de cette approche ? C'est que je peux modifier comme je veux ma classe sans toucher à l'application.

Le conteneur peut utiliser la réflexion pour les classes non enregistrées. Reprenons l'exemple précédent sous cette forme :

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

2. {

3. class Expression {

4. public function coucou()    {

5. return 'Coucou toi !';

6. }

7. public function dire($texte)    {

8. return 'Je dis ' . $texte;

9. }

10. };

11. $expression =  App::make('Expression');

12. echo $expression->coucou(), '<br>', $expression->dire('que je vais bien');

13. });

Cette fois je n'ai pas utilisé la méthode bind pour enregistrer la classe dans le conteneur. Mais la réflexion PHP permet de retrouver automatiquement la classe .

Supposons maintenant que notre classe implémente une interface. On va créer une liaison en enregistrant le type abstrait (l'interface) et le type concret (la classe) :

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

2. {

3. interface TexteInterface {

4. public function dire($texte);

5. }

7. class Expression implements TexteInterface {

8. public function dire($texte)    {

9. return 'Je dis ' . $texte;

10. }

11. };

13. App::bind('TexteInterface', 'Expression');

15. $expression =  App::make('TexteInterface');

16. echo $expression->dire('que je vais bien');

17. });

De cette manière lorsque j'utilise la méthode make en désignant l'interface j'ai au retour une instance de la classe enregistrée. Vous me direz : quel intérêt ? Au lieu de mettre TexteInterface comme nom j'aurais pu mettre n'importe quoi ! Je vais enrichir l'exemple pour vous montrer l'intérêt de procéder ainsi :

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

2. {

3. interface TexteInterface {

4. public function dire($texte);

5. }

7. class Expression implements TexteInterface {

8. public function dire($texte)    {

9. return 'Je dis ' . $texte;

10. }

11. };

13. class Parole {

14. public function __construct(TexteInterface $texte) {

15. $this->texte = $texte;

16. }

17. }

19. App::bind('TexteInterface', 'Expression');

21. $parole =  App::make('Parole');

22. echo $parole->texte->dire('que je vais bien');

23. });

J'ai ajouté la classe Parole qui attend dans son constructeur un objet qui implémente l'interface TexteInterface. Comme j'ai enregistré cette interface dans le conteneur en désignant la classe Expression, je vais avoir automatiquement la création d'une instance de Expression dans Parole. Ça ne fonctionne que parce que je crée l'objet parole par l'intermédiaire du conteneur en résolution automatique.

On injecte une dépendance dans la classe Parole, de cette façon cette classe n'a pas à se préoccuper de la manière dont est fabriqué ce qu'elle va utiliser. Le fait de définir cette dépendance comme une interface permet de l'implémenter comme on veut. Il suffit de changer une ligne du code ! Si vous regardez le code de Laravel vous verrez que ce principe a été appliqué partout.

Le fait d'utiliser des interfaces rend le code plus clair, plus facile à maintenir et à tester. Une interface est un peu un « contrat » qui indique comment implémenter les fonctionnalités. Ici l'interface TexteInterface oblige à prévoir une méthode dire. Au niveau de l'enregistrement on indique que ce « contrat » est rempli par la classe Expression. On peut facilement changer cela avec une autre classe qui respecte le « contrat » passé par l'interface. La classe Parole ignore tout de ces manipulations et n'a pas à s'en soucier. Elle utilise la méthode dire de l'objet qui se crée automatiquement et qui implémente l'interface.

XXXI-E - Singleton et objet existant

Parfois on veut avoir une seule instance d'une classe, il faut utiliser alors la méthode singleton avec la même syntaxe :

App::singleton('TexteInterface', 'Expression');

Des fois un objet existe déjà et on veut l'enregistrer dans le conteneur. On a vu précédemment comment faire sans utiliser la façade. On peut également le faire à partir de la façade avec la méthode instance :

$expression = new Expression;
App::instance('TexteInterface', $expression);

XXXI-F - Les Services Providers

On peut se poser maintenant la question : où placer les enregistrements du conteneur au niveau du code ? La façon la plus élégante est d'utiliser une classe ServiceProvider. Je vais prendre comme exemple la partie chiffrement de Laravel. On trouve le code ici :

On a la classe Encrypter pour le code d'exécution et la classe EncryptionServiceProvider pour l'enregistrement dans le conteneur. Voyons comment est constituée cette dernière classe :

1. <?php namespace Illuminate\Encryption;

3. use Illuminate\Support\ServiceProvider;

5. class EncryptionServiceProvider extends ServiceProvider {

7. /**

8. * Register the service provider.

9. *

10. * @return void

11. */

12. public function register()

13. {

14. $this->app->bindShared('encrypter', function($app)

15. {

16. return new Encrypter($app['config']['app.key']);

17. });

18. }

20. }

On a une méthode register chargée de l'enregistrement dans le conteneur (et uniquement de cela ! Ne comptez pas utiliser d'autres classes de Laravel à ce niveau, la méthode boot est faite pour cela). La liaison est accomplie avec la méthode bindShared (une version de liaison partagée), avec le nom encrypter et une fonction anonyme. Dans celle-ci on retourne une instance de la classe Encrypter en transmettant comme paramètre la clé de chiffrement qui est dans le fichier de configuration. On retrouve donc ce que nous avons vu précédemment. Pour qu'il soit chargé automatiquement il est déclaré dans le fichier app/config/app.php :

1. 'providers' => array(

3. ....

5. 'Illuminate\Encryption\EncryptionServiceProvider',

7. ...

9. ),

On parle alors de fournisseur de service. On peut donc écrire ce code :

$secret = $app['encrypter']->encrypt('passe');
echo $secret;

Puisque la classe a été enregistrée dans le conteneur ça va fonctionner. Ou avec cette syntaxe équivalente :

$secret = app('encrypter')->encrypt('passe');
echo $secret;

Mais comme cette classe a aussi une façade autant l'utiliser ainsi :

$encrypt = App::make('encrypter');
$secret = $encrypt->encrypt('passe');
echo $secret;

Étant donné que la classe Encrypter est enregistrée, elle peut être injectée dans un constructeur. Regardez par exemple le fichier Cookie/Guard.php :

1. /**

2. * Create a new CookieGuard instance.

3. *

4. * @param  \Symfony\Component\HttpKernel\HttpKernelInterface  $app

5. * @param  \Illuminate\Encryption\Encrypter  $encrypter

6. * @return void

7. */

8. public function __construct(HttpKernelInterface $app, Encrypter $encrypter)

9. {

10. $this->app = $app;

11. $this->encrypter = $encrypter;

12. }

On trouve en second paramètre du constructeur une référence à la classe Encrypter qui sera instanciée automatiquement.

XXXI-G - En analysant le code de Laravel

On apprend beaucoup de choses en analysant le code de Laravel. Pour rester dans notre thème d'interfaces pour organiser le code et faire de l'injection de dépendance, voyons de plus près ce qui concerne les vues :

On trouve une interface pour la recherche des vues :

1. <?php namespace Illuminate\View;

2. interface ViewFinderInterface {

3. public function find($view);

4. public function addLocation($location);

5. public function addNamespace($namespace, $hint);

6. public function addExtension($extension);

7. }

J'ai retiré les commentaires pour ne pas trop charger la page. On trouve la définition de quatre méthodes dans cette interface. La première pour localiser une vue, la seconde pour ajouter une localisation au finder, la troisième pour ajouter un espace de nom, et la dernière pour ajouter une extension. Ces quatre méthodes sont codées dans la classe FileViewFinder :

1. <?php namespace Illuminate\View;

2. use Illuminate\Filesystem\Filesystem;

3. class FileViewFinder implements ViewFinderInterface {

4. ...

5. public function __construct(Filesystem $files, array $paths, array $extensions = null)

6. {

7. $this->files = $files;

8. $this->paths = $paths;

10. if (isset($extensions))

11. {

12. $this->extensions = $extensions;

13. }

14. }

15. ...

16. public function find($name)

17. ...

18. public function addLocation($location)

19. ...

20. public function addNamespace($namespace, $hints)

21. ...

22. public function addExtension($extension)

23. ...

J'ai encore juste mis en évidence le code important. Remarquez au passage l'injection dans le constructeur de la classe Filesystem. On trouve enfin une injection du finder dans la classe Environment :

1. public function __construct(EngineResolver $engines, ViewFinderInterface $finder, Dispatcher $events)

2. {

3. $this->finder = $finder;

4. $this->events = $events;

5. $this->engines = $engines;

7. $this->share('__env', $this);

8. }

Pour que ça fonctionne il faut évidemment que le conteneur sache qu'il doit créer ici une instance de la classe FileViewFinder. Cela est effectué dans le service provider :

1. public function registerViewFinder()

2. {

3. $this->app->bindShared('view.finder', function($app)

4. {

5. $paths = $app['config']['view.paths'];

7. return new FileViewFinder($app['files'], $paths);

8. });

9. }

Finalement la classe Environment est injectée dans le constructeur de la classe View :

1. public function __construct(Environment $environment, EngineInterface $engine, $view, $path, $data = array())

2. {

3. ...

4. $this->environment = $environment;

5. ...

6. }

Le code est ainsi bien organisé et hiérarchisé grâce au conteneur. Il est aussi facile à tester puisque chaque classe a une tâche bien délimitée.

XXXII - Chapitre 32 : Organiser son code

XXXII-A - Les trois façons d'ajouter du code

Quand on veut organiser son code personnel avec Laravel on est de prime abord un peu décontenancé. Tout est si bien organisé qu'on a du mal à décider comment s'y prendre pour ajouter quelque chose. Surtout quand on nous dit qu'on peut le mettre où on veut !

Mais où qu'on le place il faut qu'il soit accessible. On peut résumer la situation à trois cas :

XXXII-A-1 - Ajout de fonctions

Je veux juste ajouter des fonctions personnelles, par exemple pour ajouter des helpers. Dans ce cas je crée un fichier app/helpers.php avec mes fonctions. Pour que mes fonctions soient accessibles il faut que je renseigne Composer en créant une rubrique files si elle n'existe pas encore :

1. "autoload": {

2. "classmap": [

3. "app/commands",

4. "app/controllers",

5. "app/models",

6. "app/database/migrations",

7. "app/database/seeds",

8. "app/tests/TestCase.php"

9. ],

10. "files": [

11. "app/helpers.php"

12. ]

13. },

Et évidemment je fais un dumpautoload ensuite, pour que ça fonctionne .

Une autre solution équivalente consiste à déclarer le fichier dans app/start/global.php :

require_once __DIR__.'/../helpers.php';

C'est d'ailleurs la solution donnée par Taylor Otwell dans son bouquin. Mais je préfère quand même ne pas toucher au code de base de Laravel, et me contenter d'intervenir uniquement dans Composer, ce qui me paraît plus logique. Mais vous avez le choix !

XXXII-A-2 - Ajout de classes

Supposons maintenant que je veuille ajouter quelques classes. Je crée un dossier app/lib pour les ranger. Ensuite j'ajoute mon dossier dans la rubrique classmap :

1. "autoload": {

2. "classmap": [

3. "app/commands",

4. "app/controllers",

5. "app/models",

6. "app/database/migrations",

7. "app/database/seeds",

8. "app/tests/TestCase.php",

9. "app/lib",

10. ]

11. },

Avec un petit dumpautoload toutes mes classes dans ce dossier seront reconnues.

XXXII-A-3 - PSR-0

Maintenant prenons le cas le plus riche. J'organise mon code dans des dossiers hiérarchisés en suivant les préconisations psr-0. C'est-à-dire que j'utilise des espaces de noms qui correspondent à l'architecture de mes dossiers. Voici la syntaxe dans ce cas :

1. "autoload": {

2. "classmap": [

3. "app/commands",

4. "app/controllers",

5. "app/models",

6. "app/database/migrations",

7. "app/database/seeds",

8. "app/tests/TestCase.php"

9. ],

10. "psr-0": {

11. "MesClasses\\": "app"

12. }

13. },

Après un dumpautoload, mes classes organisées dans mon espace de nom MesClasses et situées dans le dossier app seront reconnues.

XXXII-B - Un cas d'exemple

Pour voir ces trois possibilités en action je vais prendre un exemple simple d'application. À partir d'une installation neuve de Laravel et après avoir configuré une base de données, on crée une table livres avec cette migration :

1. class CreateLivresTable extends Migration {

3. public function up()

4. {

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

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

7. $table->timestamps();

8. $table->string('titre', 100);

9. $table->string('auteur', 100);

10. $table->string('editeur', 100);

11. });

12. }

14. public function down()

15. {

16. Schema::drop('livres');

17. }

18. }

Donc on obtient une simple table avec un ID, trois champs de données et les timestamps. Pas vraiment réaliste évidemment, mais le but est juste de montrer l'organisation du code, pas de créer une application réelle. Pour les besoins des tests je crée aussi quelques enregistrements :

1. class DatabaseSeeder extends Seeder {

3. public function run()

4. {

6. Eloquent::unguard();

8. for ($i = 1; $i < 21; $i++) {

9. DB::table('livres')->insert(

10. array(

11. 'titre' => 'Titre ' . $i,

12. 'auteur' => 'Auteur ' . $i,

13. 'editeur' => 'Editeur ' . $i

14. )

15. );

16. }

18. }

20. }

J'ai maintenant vingt enregistrements à disposition. Il ne me reste plus qu'à créer le code pour gérer tout ça. Il me faut un modèle Livre :

1. class Livre extends Eloquent {

3. protected $table = 'livres';

4. public $timestamps = true;

5. protected $softDelete = false;

6. protected $guarded = array('id');

8. }

On ajoute un contrôleur qui va se contenter d'afficher les livres avec une pagination et de permettre l'ajout de livres avec une validation :

1. class LivreController extends BaseController {

3. public function getIndex()

4. {

5. $livres = Livre::paginate(5);

6. return View::make('hello')->with('livres', $livres);

7. }

9. public function getCreate()

10. {

11. return View::make('saisie');

12. }

14. public function postCreate()

15. {

17. $rules = array(

18. 'titre' => 'required|alpha|unique:livres',

19. 'auteur' => 'required|alpha',

20. 'editeur' => 'required|alpha'

21. );

23. $validator = Validator::make(Input::all(), $rules);

25. if ($validator->passes()) {

26. $livre = new Livre;

27. $livre->create(Input::all());

28. return 'Livre enregistré';

29. } else {

30. return Redirect::back()->withInput()->withErrors($validator->messages());

31. }

33. }

35. }

Comme j'ai prévu un contrôleur RESTful la route est élémentaire :

Route::controller('livres', 'LivreController');

Il ne manque plus que la vue hello :

1. <!doctype html>

2. <html lang="fr">

3. <head>

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

5. <title>Ou placer mon code</title>

6. </head>

7. <body>

8. {{ link_to_action('LivreController@getCreate', 'Ajouter un livre') }}

9. @foreach ($livres as $livre)

10. <h1>Titre du livre : {{ $livre->titre }}</h1>

11. <p>Auteur : {{ $livre->auteur }} </p>

12. <p>Editeur :  {{ $livre->editeur }}</p>

13. @endforeach

14. {{ $livres->links(); }}

15. </body>

16. </html>

Et la vue saisie :

1. <!doctype html>

2. <html lang="fr">

3. <head>

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

5. <title>Ou placer mon code</title>

6. </head>

7. <body>

8. @if ($errors->count())

9. <p>Les erreurs suivantes sont survenues :</p>

10. <ul>

11. @foreach($errors->all() as $message)

12. <li>{{ $message }}</li>

13. @endforeach

14. </ul>

15. @endif

16. {{ Form::open(array('url' => 'livres/create')) }}

17. {{ Form::label('titre', 'Titre :')}}

18. {{ Form::text('titre') }}

19. {{ Form::label('auteur', 'Auteur :')}}

20. {{ Form::text('auteur') }}

21. {{ Form::label('auteur', 'Editeur :')}}

22. {{ Form::text('editeur') }}

23. {{ Form::submit('Envoyer') }}

24. {{ Form::close() }}

25. </body>

26. </html>

Tout ça est rapide à mettre en place et fonctionne à merveille. À partir de cette situation, je vais envisager plusieurs cas d'organisation du codage pour mettre en lumière ce que j'ai cité au départ de ce fil.

XXXII-C - La validation dans des fonctions

Si j'ai plusieurs formulaires dans une application, je risque de multiplier les cas de validation. Au bout d'un moment je me dis que ça serait bien de rassembler toutes ces validations quelque part. Alors je crée un fichier app/validation.php pour le faire. Dans mon cas évidemment je n'ai qu'une validation, alors je construis mon code dans ce fichier :

1. function validateLivre($inputs)

2. {

3. $rules = array(

4. 'titre' => 'required|alpha|unique:livres',

5. 'auteur' => 'required|alpha',

6. 'editeur' => 'required|alpha'

7. );

9. return validate($inputs, $rules);

11. }

13. function validate($inputs, $rules)

14. {

16. $validator = Validator::make($inputs, $rules);

18. if ($validator->passes()) {

19. return true;

20. } else {

21. return $validator->messages();

22. }

24. }

J'ai prévu une fonction validateLivre qui a comme unique paramètre les entrées du formulaire et qui contient les règles. J'ai ensuite une fonction validate qui accomplit la validation et qui renvoie l'information. Pour que mes fonctions soient connues de l'application j'informe Composer :

1. "autoload": {

2. "classmap": [

3. "app/commands",

4. "app/controllers",

5. "app/models",

6. "app/database/migrations",

7. "app/database/seeds",

8. "app/tests/TestCase.php"

9. ],

10. "files": [

11. "app/validation.php"

12. ]

13. },

Après un dumpautoload je peux alléger la méthode postCreate de mon contrôleur LivreController :

1. public function postCreate()

2. {

4. $validate = validateLivre(Input::all());

6. if($validate === true)

7. {

8. $livre = new Livre;

9. $livre->create(Input::all());

10. return 'Livre enregistré';

11. } else {

12. return Redirect::back()->withInput()->withErrors($validate);

13. }

15. }

Maintenant, tout ce qui concerne la validation est rassemblé dans un seul fichier, et j'ai pu factoriser quelques actions. Ce n'est pas forcément très joli mais on va améliorer tout ça bientôt…

XXXII-D - La validation dans des classes

Supposons maintenant que la solution vue plus haut ne me convienne pas. Je peux placer mon code dans des classes. Pour localiser ces classes je crée un dossier app/validation. J'explique à Composer qu'il doit charger mes classes :

1. "autoload": {

2. "classmap": [

3. "app/commands",

4. "app/controllers",

5. "app/models",

6. "app/database/migrations",

7. "app/database/seeds",

8. "app/tests/TestCase.php",

9. "app/validation"

10. ]

11. },

Ensuite je réfléchis un peu. Dans mon application je vais avoir sans doute plusieurs formulaires et je vais donc avoir du code en commun. Je vais donc créer une classe abstraite app/validation/ValidationBase avec ce code commun :

1. abstract class ValidationBase

2. {

4. protected $rules;

6. public function validate($inputs)

7. {

8. $validator = Validator::make($inputs, $this->rules);

10. if ($validator->passes()) {

11. return true;

12. } else {

13. return $validator->messages();

14. }

15. }

17. }

Je prévois donc dans cette classe la méthode pour la validation, qui prend comme paramètre les entrées du formulaire. J'ai aussi une propriété $rules qui sera différente selon le formulaire. Pour mon cas je crée donc une classe app/validation/ValidationLivre qui hérite de la précédente :

1. class ValidationLivre extends ValidationBase

2. {

4. protected $rules = array(

5. 'titre' => 'required|alpha|unique:livres',

6. 'auteur' => 'required|alpha',

7. 'editeur' => 'required|alpha'

8. );

10. }

Je me retrouve pour cette classe avec seulement la propriété $rules renseignée.

Je dois ensuite modifier mon contrôleur pour tenir compte de ces changements :

1. public function postCreate()

2. {

4. $val = new ValidationLivre();

6. $validate = $val->validate(Input::all());

8. if($validate === true)

9. {

10. $livre = new Livre;

11. $livre->create(Input::all());

12. return 'Livre enregistré';

13. } else {

14. return Redirect::back()->withInput(Input::all())->withErrors($validate);

15. }

17. }

Dans la méthode postCreate je crée une instance de la classe ValidationLivre, puis j'utilise la méthode validate en transmettant les entrées. Tout ça fonctionne très bien. Mais ce n'est pas très propre. Il serait plus élégant d'utiliser le conteneur de Laravel pour pourvoir injecter la classe ValidationLivre dans le contrôleur plutôt que de créer une instance dans le code. Le conteneur de Laravel est suffisamment intelligent pour résoudre des liaisons même si on ne les a pas déclarées. Je vais donc injecter ma classe dans le contrôleur et voir si ça fonctionne :

1. class LivreController extends BaseController {

3. protected $val;

5. public function __construct(ValidationLivre $val)

6. {

7. $this->val = $val;

8. }

10. public function getIndex()

11. {

12. $livres = Livre::paginate(5);

13. return View::make('hello')->with('livres', $livres);

14. }

16. public function getCreate()

17. {

18. return View::make('saisie');

19. }

21. public function postCreate()

22. {

24. $validate = $this->val->validate(Input::all());

26. if($validate === true)

27. {

28. $livre = new Livre;

29. $livre->create(Input::all());

30. return 'Livre enregistré';

31. } else {

32. return Redirect::back()->withInput(Input::all())->withErrors($validate);

33. }

35. }

37. }

J'ai ajouté un constructeur avec un paramètre de type ValidationLivre. Le conteneur de Laravel cherche la classe et la trouve ! En effet les contrôleurs sont toujours résolus dans le conteneur, ce qui permet d'injecter un type. Il me crée une instance et je peux l'utiliser . Mon code devient plus propre et plus facile à tester.

XXXII-E - Les espaces de noms

XXXII-E-1 - psr-0 kesaco ?

Il n'est pas très prudent de créer des classes sans les placer dans des espaces de noms. D'autre part il est conseillé d'utiliser les préconisations du psr-0. Mais c'est quelque chose de finalement assez récent pour les développeurs PHP. C'est une préconisation qui a été établie par les principaux acteurs du PHP pour unifier la présentation des librairies. En gros ça permet de pouvoir assurer le chargement automatique des classes grâce à une convention de nommage et ainsi de constituer facilement une application avec des « briques » issues de différentes provenances.

Le plus important est de comprendre l'organisation des espaces de noms :

\<Nom du Vendor>\(<Espace de noms>\)*<Nom de la Classe>

Si vous regardez le code de Laravel vous allez voir que la norme a été parfaitement appliquée.

XXXII-E-2 - Utilisation de psr-0 pour l'exemple

Je vais poursuivre mon exemple en utilisant cette possibilité. Je vais créer l'architecture app/lib/validation et je vais indiquer à Composer que je respecte le psr-0 :

1. "autoload": {

2. "classmap": [

3. "app/commands",

4. "app/controllers",

5. "app/models",

6. "app/database/migrations",

7. "app/database/seeds",

8. "app/tests/TestCase.php"

9. ],

10. "psr-0": {

11. "Lib\\": "app"

12. }

13. },

Mon espace de noms est donc Lib\Validation. Comme je n'ai pas grand-chose à mettre dedans je n'aurai pas de sous-espace.

Pour faire les choses bien, je commence par créer une interface app/lib/validation/ValidationInterface :

1. <?php namespace Lib\Validation;

3. interface ValidationInterface {

5. public function validate(array $inputs);

7. }

Le contrat est établi. Je veux une méthode de validation. Je crée ma classe abstraite app/lib/validation/ValidationBase comme précédemment mais évidemment modifiée :

1. <?php namespace Lib\Validation;

3. use Illuminate\Validation\Factory as Validator;

5. abstract class ValidationBase implements ValidationInterface

6. {

8. protected $rules;

10. protected $validator;

12. public function __construct(Validator $validator)

13. {

14. $this->validator = $validator;

15. }

17. public function validate(array $inputs)

18. {

19. $v = $this->validator->make($inputs, $this->rules);

21. if ($v->passes()) {

22. return true;

23. } else {

24. return $v->messages();

25. }

26. }

28. }

Cette fois j'injecte le validateur de Laravel dans le constructeur. Je crée ensuite ma classe de validation concrète app/lib/validation/ValidationLivre pour les livres :

1. <?php namespace Lib\Validation;

3. class ValidationLivre extends ValidationBase

4. {

6. protected $rules = array(

7. 'titre' => 'required|alpha|unique:livres',

8. 'auteur' => 'required|alpha',

9. 'editeur' => 'required|alpha'

10. );

12. }

Je me retrouve donc avec cette architecture :

Et évidemment dans le contrôleur je dois tenir compte de l'espace de noms :

1. <?php

3. use Lib\Validation\ValidationLivre;

4. class LivreController extends BaseController {

6. protected $val;

8. public function __construct(ValidationLivre $val)

9. {

10. $this->val = $val;

11. }

13. public function getIndex()

14. {

15. $livres = Livre::paginate(5);

16. return View::make('hello')->with('livres', $livres);

17. }

19. public function getCreate()

20. {

21. return View::make('saisie');

22. }

24. public function postCreate()

25. {

26. $validate = $this->val->validate(Input::all());

28. if($validate === true)

29. {

30. $livre = new Livre;

31. $livre->create(Input::all());

32. return 'Livre enregistré';

33. } else {

34. return Redirect::back()->withInput(Input::all())->withErrors($validate);

35. }

37. }

39. }

Comme j'injecte une classe concrète, c'est résolu automatiquement. Bon, ça commence à avoir un peu plus d'allure comme ça .

XXXII-F - Délocaliser la gestion du modèle

On peut pousser un peu plus la réflexion à partir de notre exemple. Un contrôleur sert à quoi ? Essentiellement à récupérer les informations de l'utilisateur et à générer une réponse. Tout le traitement intermédiaire ne devrait pas le concerner. Nous avons déjà extrait la validation. Par contre on voit que l'action sur le modèle est encore inclus dans le contrôleur, ce qui ne devrait pas être sa tâche. Je ne devrais pas avoir à créer une instance du modèle Livre ici. Dans l'idéal je veux un contrôleur comme ça :

1. <?php

3. use Lib\Validation\ValidationLivre;

4. use Lib\Gestion\GestionLivre;

6. class LivreController extends BaseController {

8. protected $val;

9. protected $gestion;

11. public function __construct(ValidationLivre $val, GestionLivre $gestion)

12. {

13. $this->val = $val;

14. $this->gestion = $gestion;

15. }

17. public function getIndex()

18. {

19. $livres = $this->gestion->affiche(5);

20. return View::make('hello')->with('livres', $livres);

21. }

23. public function getCreate()

24. {

25. return View::make('saisie');

26. }

28. public function postCreate()

29. {

31. $validate = $this->val->validate(Input::all());

33. if($validate === true)

34. {

35. $this->gestion->create(Input::all());

36. return 'Livre enregistré';

37. } else {

38. return Redirect::back()->withInput(Input::all())->withErrors($validate);

39. }

41. }

43. }

Vous remarquez que j'ai ajouté l'espace de nom Lib\Gestion\GestionLivre. C'est ici que je vais assurer toute l'intendance du modèle. Je me contente ensuite d'une injection dans le contrôleur :

public function __construct(ValidationLivre $val, GestionLivre $gestion)

Pour afficher les livres, je fais simplement appel à la méthode affiche :

$livres = $this->gestion->affiche(5);

Et ensuite pour créer le livre, j'appelle la méthode create et c'est bouclé :

$this->gestion->create(Input::all());

C'est propre, efficace, et beaucoup plus simple à tester ! Mais bon, pour que ça marche je dois créer quelques classes. D'abord j'enrichis mon espace de noms avec Lib\Gestion\GestionLivre. Comme je veux faire quelque chose de propre je commence par une interface app/lib/gestion/GestionInterface :

1. <?php namespace Lib\Gestion;

3. interface GestionInterface {

5. public function affiche($pages);

6. public function create(array $inputs);

8. }

Ensuite je continue avec une classe abstraite app/lib/gestion/GestionBase qui sera commune à tous les modèles :

1. <?php namespace Lib\Gestion;

3. abstract class GestionBase implements GestionInterface

4. {

6. protected $model;

8. public function affiche($pages)

9. {

10. return $this->model->paginate($pages);

11. }

13. public function create(array $inputs)

14. {

15. $this->model->create($inputs);

16. }

18. }

Et enfin j'établis ma classe spécifique aux livres app/lib/gestion/GestionLivre :

1. <?php namespace Lib\Gestion;

3. class GestionLivre extends GestionBase

4. {

6. public function __construct(\Livre $livre)

7. {

8. $this->model = $livre;

9. }

11. }

Je me retrouve donc maintenant avec cette architecture :

Mon contrôleur est propre, et fait uniquement ce qu'il doit faire sans se soucier de l'intendance intermédiaire. Le code est bien organisé et facile à comprendre et à modifier. En plus il permet une extension facile. Si je dois créer un autre formulaire avec un autre modèle, j'ai déjà tout ce qu'il faut pour le faire .

XXXII-G - Un service de validation

Faisons un pas de plus dans l'organisation du code. Regardons le constructeur de notre contrôleur :

public function __construct(ValidationLivre $val, GestionLivre $gestion)

On se rend compte qu'on injecte des classes concrètes. Elle sont résolues automatiquement mais posons-nous une question : puisque nous avons une interface il serait sans doute plus judicieux d'injecter celle-ci. Quel intérêt ? Une interface représente un contrat, c'est une façon de dire au contrôleur : tu as la possibilité d'effectuer une validation de telle façon. Dans notre cas très simple on lui dit juste qu'il existe une méthode validate. Il n'a pas à savoir comment la validation est réalisée. On pourrait imaginer une tout autre organisation du code : si on injecte l'interface, le contrôleur fonctionnera toujours. Voici le contrôleur modifié en conséquence :

1. use Lib\Validation\ValidationInterface;

2. use Lib\Gestion\GestionLivre;

4. class LivreController extends BaseController {

6. protected $val;

7. protected $gestion;

9. public function __construct(ValidationInterface $val, GestionLivre $gestion)

10. {

11. $this->val = $val;

12. $this->gestion = $gestion;

13. }

15. public function getIndex()

16. {

17. $livres = $this->gestion->affiche(5);

18. return View::make('hello')->with('livres', $livres);

19. }

21. public function getCreate()

22. {

23. return View::make('saisie');

24. }

26. public function postCreate()

27. {

29. $validate = $this->val->validate(Input::all());

31. if($validate === true)

32. {

33. $this->gestion->create(Input::all());

34. return 'Livre enregistré';

35. } else {

36. return Redirect::back()->withInput(Input::all())->withErrors($validate);

37. }

39. }

41. }

On injecte l'interface ValidationInterface. Si on teste le code on va recevoir une erreur, parce que le conteneur ne sait pas quelle classe concrète correspond à cette interface. Il faut donc l'en informer. Pour cela il faut créer un ServiceProvider :

1. <?php namespace Lib\Validation;

3. use Illuminate\Support\ServiceProvider;

5. class ValidationServiceProvider extends ServiceProvider {

7. public function register()

8. {

9. $this->app->bind('Lib\Validation\ValidationInterface', function($app)

10. {

11. return new ValidationLivre($app->make('Illuminate\Validation\Factory'));

12. });

13. }

15. }

Ici on crée un lien entre l'interface ValidationInterface et la classe concrète ValidationLivre. Il faut aussi renseigner le fichier app/config/app.php :

'Illuminate\Workbench\WorkbenchServiceProvider',
'Lib\Validation\ValidationServiceProvider'
),

On fait un dumpautoload pour que le lien soit enregistré, et ça devrait fonctionner.

XXXIII - Chapitre 33 : Les relations avec Eloquent 1/2

Eloquent est un ORM (Object-Relational Mapping) élégant et efficace. Son utilité essentielle se trouve dans le traitement de données relationnelles. Il est parfois délicat de le mettre en œuvre, surtout pour ceux qui ne sont pas vraiment habitués aux subtilités du modèle relationnel. Dans cet article, je vais m'attacher à présenter les bases de ce domaine avec l'application d'Eloquent. Je vais faire un tour d'horizon complet. Alors c'est parti pour une visite guidée.

Dans cette première partie je vais présenter la construction des relations, je traiterai les problèmes de gestion des enregistrements liés dans un prochain chapitre.

XXXIII-A - La base d'exemple

Pour que le voyage soit efficace on va avoir besoin d'une base de données pour tester les différents cas de figure. J'ai créé un modèle de base sur le site laravelsd.com :

Ce site propose un outil simple et efficace pour créer des tables et un schéma relationnel de façon visuelle. La base que j'ai créée couvre tous les cas possibles :

Ce n'est évidemment pas une situation très réaliste, son seul but est de donner l'occasion de présenter toutes les situations utiles pour cet article. L'outil génère gentiment les migrations, les modèles et même des vues ! Vous pouvez récupérer tout ça en cliquant simplement sur ce bouton :

Alors avec une version neuve de Laravel, et après avoir créé et configuré une base, effectuez les migrations téléchargées et copiez les modèles. Comme il va nous falloir des enregistrements pour les tests, j'ai créé ce seed :

1. class DatabaseSeeder extends Seeder {

3. /**

4. * Run the database seeds.

5. *

6. * @return void

7. */

8. public function run()

9. {

11. Eloquent::unguard();

13. for ($i = 1; $i < 11; $i++) {

14. DB::table('categories')->insert(array('nom' => 'Categorie ' . $i));

15. DB::table('periodes')->insert(array('nom' => 'Periode ' . $i));

16. DB::table('pays')->insert(array('nom' => 'Pays ' . $i));

17. }

19. for ($i = 1; $i < 21; $i++) {

20. DB::table('themes')->insert(array('nom' => 'Theme ' . $i));

21. DB::table('editeurs')->insert(array('nom' => 'Editeur ' . $i));

22. DB::table('autoedites')->insert(array('nom' => 'Autoedite ' . $i));

23. DB::table('contacts')->insert(array('telephone' => '00 00 00 00 00', 'editeur_id' => $i));

24. DB::table('villes')->insert(array('nom' => 'Ville ' . $i, 'pays_id' => rand(1, 10)));

25. }

27. for ($i = 1; $i < 21; $i++) {

28. DB::table('auteurs')->insert(array('nom' => 'Auteur ' . $i, 'ville_id' => rand(1, 20)));

29. }

31. for ($i = 1; $i < 41; $i++) {

32. $choix = array('Autoedite','Editeur');

33. DB::table('livres')->insert(array(

34. 'titre' => 'Titre ' . $i,

35. 'livrable_id' => rand(1, 20),

36. 'theme_id' => rand(1, 20),

37. 'livrable_type' => $choix[rand(0,1)]

38. ));

39. }

41. for ($i = 1; $i < 21; $i++) {

42. $number = rand(2, 8);

43. for ($j = 1; $j <= $number; $j++) {

44. DB::table('auteur_livre')->insert(array(

45. 'livre_id' => rand(1, 40),

46. 'auteur_id' => $i

47. ));

48. }

49. }

51. for ($i = 1; $i < 81; $i++) {

52. $choix = array('Categorie','Periode');

53. DB::table('themables')->insert(array(

54. 'theme_id' => rand(1, 20),

55. 'themable_id' => rand(1, 10),

56. 'themable_type' => $choix[rand(0,1)]

57. ));

58. }

60. }

61. }

Là aussi c'est un remplissage artificiel mais efficace . Il peut y avoir des redondances parasites mais c'est admissible pour des tests. Maintenant que l'intendance est en place voyons un peu tout ça en détail…

XXXIII-B - La relation 1:1

Cette relation est la plus simple mais la moins utile. De quoi s'agit-il ? Prenons le cas de notre exemple :

J'ai une table editeurs et une table contacts. Un éditeur a un numéro de téléphone, celui-ci se trouve enregistré dans la table contact. Comment est établie la relation ? On trouve dans la table contacts le champ editeur_id qui correspond à l'ID de l'éditeur. Je l'ai surligné pour mieux le visualiser. C'est comme cela qu'on peut savoir qu'un numéro de téléphone dans la table contacts correspond à un éditeur dans la table editeurs. On appelle ce champ clé étrangère parce que c'est une valeur qui appartient à une autre table et qui est juste insérée ici pour la relation.

Il ne faut pas analyser longtemps cette situation pour se rendre compte qu'on aurait tout aussi bien pu prévoir le champ telephone dans la table editeurs ! C'est toujours le cas avec une relation de type 1:1 mais alors pourquoi créer deux tables ? En général c'est pour des raisons de performance : si on a des données volumineuses auxquelles on accède rarement, il devient alors intéressant de les mettre dans une table séparée. Autrement dit vous ne rencontrerez pas souvent cette situation ! Mais son intérêt est qu'elle est facile à comprendre et constitue une bonne introduction aux suivantes.

Pour comprendre une relation, il faut se positionner dans une table (en pensée ) et se poser les bonnes questions. On a donc deux cas.

XXXIII-B-1 - hasOne

Je suis dans la table editeurs. Je regarde la table contacts et je me dis « j'ai là un numéro de téléphone ». En langage Eloquent on traduit ça par hasOne :

Pour traduire cela de façon concrète, on crée dans le modèle Editeur une méthode :

1. public function contact()

2. {

3. return $this->hasOne('Contact');

4. }

Pour établir la relation, Eloquent part du principe que la clé étrangère est construite à partir du nom du modèle auquel on adjoint « _id », ce qui donne bien « editeur_id ». On a d'autres possibilités, regardez la méthode dans la classe Model :

1. public function hasOne($related, $foreignKey = null, $localKey = null)

2. {

3. $foreignKey = $foreignKey ?: $this->getForeignKey();

5. $instance = new $related;

7. $localKey = $localKey ?: $this->getKeyName();

9. return new HasOne($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);

10. }

Le deuxième paramètre permet de renseigner la clé étrangère si elle ne respecte pas la norme, donc on peut écrire par exemple :

1. public function contact()

2. {

3. return $this->hasOne('Contact', 'editeur');

4. }

pour le cas où la clé étrangère se nomme « editeur ». Vous remarquez aussi qu'on dispose d'un troisième paramètre, il sert à renseigner le nom de l'ID de la table d'origine si celui-ci ne se nomme pas « id ». Le cas peut se présenter si vous utilisez Eloquent sur une table existante et que vous ne désirez pas tout changer. Je peux maintenant écrire cela :

$telephone = Editeur::first()->contact()->first()->telephone;

Et je trouve le numéro de téléphone de l'éditeur sélectionné. Eloquent génère ces deux requêtes :

select * from `editeurs` limit 1
select * from `contacts` where `contacts`.`editeur_id` = '1' limit 1

Avec le query builder ce serait plus laborieux pour obtenir le même résultat :

$editeur = DB::table('editeurs')->first();
$contact = DB::table('contacts')->where('editeur_id', $editeur->id)->first();
$telephone =  $contact->telephone;

Comme Eloquent est très conciliant vous pouvez même écrire cela :

$telephone = Editeur::first()->contact->telephone;

Ce qui rend la syntaxe particulièrement élégante et lisible !

XXXIII-B-2 - belongsTo

Envisageons maintenant l'inverse. Cette fois je me place dans la table des contacts. Là je me dis « j'ai un numéro de téléphone qui appartient à un éditeur ». En langage Eloquent on traduit ça par belongsTo :

Pour traduire cela de façon concrète, on crée dans le modèle Editeur une méthode :

1. public function editeur()

2. {

3. return $this->belongsTo('Editeur');

4. }

Attention ! Pour établir la relation, Eloquent cette fois utilise le nom de la méthode ! La clé étrangère est construite à partir du nom de la méthode auquel on adjoint « _id », ce qui donne bien editeur_id dans notre cas. On a d'autres possibilités, regardez la méthode dans la classe Model :

1. public function belongsTo($related, $foreignKey = null, $otherKey = null, $relation = null)

2. {

3. // If no relation name was given, we will use this debug backtrace to extract

4. // the calling method's name and use that as the relationship name as most

5. // of the time this will be what we desire to use for the relatinoships.

6. if (is_null($relation))

7. {

8. list(, $caller) = debug_backtrace(false);

10. $relation = $caller['function'];

11. }

13. // If no foreign key was supplied, we can use a backtrace to guess the proper

14. // foreign key name by using the name of the relationship function, which

15. // when combined with an "_id" should conventionally match the columns.

16. if (is_null($foreignKey))

17. {

18. $foreignKey = snake_case($relation).'_id';

19. }

21. $instance = new $related;

23. // Once we have the foreign key names, we'll just create a new Eloquent query

24. // for the related models and returns the relationship instance which will

25. // actually be responsible for retrieving and hydrating every relations.

26. $query = $instance->newQuery();

28. $otherKey = $otherKey ?: $instance->getKeyName();

30. return new BelongsTo($query, $this, $foreignKey, $otherKey, $relation);

31. }

On peut donc renseigner le nom de la clé étrangère si elle ne correspond pas à la norme :

1. public function editeur()

2. {

3. return $this->belongsTo('Editeur', 'editeur');

4. }

Vous pouvez aussi avec le troisième paramètre indiquer le nom de la clé de la table editeurs si elle ne s'appelle pas « id ».

Je peux maintenant écrire cela :

$nom = Contact::find(5)->editeur()->first()->nom;

Et je trouve le nom de l'éditeur pour le contact sélectionné. Eloquent génère ces deux requêtes :

select * from `contacts` where `id` = '5' limit 1
select * from `editeurs` where `editeurs`.`id` = '5' limit 1

Comme Eloquent est très conciliant vous pouvez même écrire cela :

$nom = Contact::find(5)->editeur->nom;

Ce qui rend la syntaxe particulièrement élégante et lisible !

XXXIII-C - La relation 1:n

La relation 1:n est de loin la plus courante ! Dans la base d'exemple on trouve cette relation entre les pays et les villes :

J'ai une table pays et une table villes. Un pays peut avoir plusieurs villes, par contre une ville est forcément située dans un seul pays. Ce type de relation est dissymétrique, contrairement à la relation 1:1 que nous avons vu ci-dessus.

Comment est établie la relation ? On trouve dans la table villes le champ pays_id qui correspond à l'id du pays. Je l'ai entouré pour mieux le visualiser. C'est comme cela qu'on peut savoir qu'une ville correspond à un certain pays. On appelle ce champ « clé étrangère » parce que c'est une valeur qui appartient à une autre table et qui est juste insérée ici pour la relation.

On va à nouveau envisager la relation en se positionnant dans chacune des tables.

XXXIII-C-1 - hasMany

Commençons par nous situer dans la table des pays. Je me dis « j'ai beaucoup de villes » :

Pour traduire cela de façon concrète on crée dans le modèle Pays une méthode :

1. public function villes()

2. {

3. return $this->hasMany('Ville');

4. }

Voyons cette méthode dans la classe Model :

1. public function hasMany($related, $foreignKey = null, $localKey = null)

2. {

3. $foreignKey = $foreignKey ?: $this->getForeignKey();

5. $instance = new $related;

7. $localKey = $localKey ?: $this->getKeyName();

9. return new HasMany($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);

10. }

Si la clé étrangère n'est pas renseignée, elle est construite à partir du nom du modèle. Dans notre cas on a donc « pays_id ». Vous avez aussi la possibilité de renseigner le deuxième paramètre si votre champ ne correspond pas à la norme :

1. public function villes()

2. {

3. return $this->hasMany('Ville', 'pays');

4. }

Le troisième paramètre sert à indiquer le nom de la clé pour la table des pays si ce n'est pas « id », comme on l'a déjà vu pour les autres méthodes ci-dessus.

Je peux maintenant écrire cela :

1. Pays::find(4)->villes->each(function($ville)

2. {

3. echo $ville->nom, '<br>';

4. });

Je trouve toutes les villes pour le pays d'id 5. J'itère dans la collection obtenue pour récupérer les noms des villes. Eloquent génère ces deux requêtes :

select * from `pays` where `id` = '4' limit 1
select * from `villes` where `villes`.`pays_id` = '4'

Dans mon cas j'obtiens ce résultat :

Ville 1
Ville 2
Ville 10

Vous pouvez évidemment avoir un résultat différent étant donné que j'ai rempli les tables avec des valeurs aléatoires.

XXXIII-C-2 - belongsTo

Voyons maintenant la relation à partir de la table des villes :

Là je me dis que « j'appartiens à un seul pays ». On retrouve donc le belongsTo qu'on à déjà vu pour la relation de type 1:1. La situation est strictement la même et donc le traitement identique. Tout ce que j'ai dit ci-dessus est donc valable aussi pour la relation de type 1:n. En fait Eloquent est totalement ignorant du type de relation, son seul repère est la méthode utilisée. Donc quand vous employez la méthode belongsTo il ne sait absolument pas si vous avez mis en place une relation 1:1 ou 1:n et il n'en a pas besoin. Il agit de façon localisée.

XXXIII-C-3 - hasManyThrough

Voyons maintenant une extension de hasMany avec hasManyThrough. Regardez cette partie de la base d'exemple :

On a la relation 1:n vue ci-dessus entre les pays et les villes. On a le même type de relation entres les villes et les auteurs. La méthode hasManyThrough permet de « traverser » la table des villes pour mettre directement en relation la table des pays avec celle des auteurs :

Autrement dit on veut tous les auteurs d'un pays. Pour traduire cela de façon concrète, on crée dans le modèle Pays une méthode auteurs :

1. public function auteurs()

2. {

3. return $this->hasManyThrough('Auteur', 'Ville');

4. }

Voyons cette méthode dans la classe Model :

1. public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null)

2. {

3. $through = new $through;

5. $firstKey = $firstKey ?: $this->getForeignKey();

7. $secondKey = $secondKey ?: $through->getForeignKey();

9. return new HasManyThrough(with(new $related)->newQuery(), $this, $through, $firstKey, $secondKey);

10. }

Analysons les paramètres :

1. C'est le modèle de destination, dans notre cas Auteur ;

2. C'est le modèle intermédiaire, dans notre cas Ville ;

3. Là on peut mettre la clé étrangère dans la table intermédiaire si elle n'est pas à la norme, nous on a bien pays_id ;

4. Là on peut mettre la clé étrangère dans la table finale si elle n'est pas à la norme, nous on a bien ville_id.

Maintenant on peut écrire ça :

1. Pays::find(4)->auteurs->each(function($auteur)

2. {

3. echo $auteur->nom, '<br>';

4. });

J'obtiens ce résultat dans mon cas :

1. Auteur 2

2. Auteur 6

3. Auteur 13

4. Auteur 19

5. Auteur 14

6. Auteur 8

7. Auteur 15

Eloquent s'en sort avec deux requêtes dont une jointure :

select * from `pays` where `id` = '4' limit 1
select `auteurs`.*, `villes`.`pays_id` from `auteurs` inner join `villes` on `villes`.`id` = `auteurs`.`ville_id` where `villes`.`pays_id` = '4'

Maintenant comment faire l'inverse de hasManyThrough ? Pas besoin d'inventer quelque chose de nouveau, il suffit d'enchaîner deux belongsTo. Par exemple ce code :

Auteur::find(2)->ville->pays->nom;

va nous donner le nom du pays pour l'auteur d'id 2. Élégant non ?

XXXIII-D - La relation n:n

La relation n:n est la plus complexe des trois. Voyons un exemple dans la base pour comprendre de quoi il s'agit :

On a une table des auteurs et une table des livres. Un auteur peut écrire plusieurs livres et réciproquement un livre peut être écrit par plusieurs auteurs. Il est impossible de relier directement les deux tables avec des clés étrangères pour régler cette situation. On est donc obligé de créer une table intermédiaire appelée « pivot » chargée de mémoriser les deux clés étrangères, et d'effectuer ainsi la liaison entre les deux tables. Ici c'est la table auteur_livre.

Par convention la table pivot doit comporter le nom des deux modèles séparés par un soulignage (underscore). Ici j'ai choisi auteur_livre, j'aurais pu évidemment choisir aussi livre_auteur puisque la relation est strictement symétrique. On trouve dans la table pivot les deux clés étrangères :

1. La clé auteur_id qui référence un enregistrement de la table des auteurs ;

2. La clé livre_id qui référence un enregistrement de la table des livres.

XXXIII-D-1 - belongsToMany

Comme la relation est symétrique on a une seule méthode : belongsToMany. En effet si je me place du côté de la table des auteurs je me dis « j'appartiens à plusieurs livres » (bon c'est une image, il ne faut pas que les auteurs se sentent possédés par leurs œuvres ) et réciproquement du côté de la table des livres je me dis « j'appartiens à plusieurs auteurs » :

Pour concrétiser ça, on crée une méthode livres dans le modèle Auteur :

1. public function livres()

2. {

3. return $this->belongsToMany('Livre');

4. }

Et évidemment on crée la réciproque auteurs dans le modèle Livre :

1. public function auteurs()

2. {

3. return $this->belongsToMany('Auteur');

4. }

Regardez la signature de la méthode belongsToMany :

public function belongsToMany($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null)

Les paramètres doivent vous être maintenant familiers parce qu'ils se répètent au fil des méthodes :

1. Là on met le nom du modèle de la table visée ;

2. Là on met le nom de la table pivot s'il n'est pas conventionnel, nous on a bien choisi auteur_livre (livre_auteur aurait aussi marché) ;

3. Là on met le nom de la clé étrangère de la table de départ s'il n'est pas conventionnel ;

4. Là on met le nom de la clé étrangère de la table d'arrivée s'il n'est pas conventionnel.

Maintenant si on veut connaître tous les titres des livres d'un auteur c'est facile :

1. Auteur::find(4)->livres->each(function($livre)

2. {

3. echo $livre->titre, '<br>';

4. });

Dans mon cas j'obtiens :

1. Titre 8

2. Titre 24

3. Titre 7

4. Titre 39

Voici les requêtes générées :

select * from `auteurs` where `id` = '4' limit 1
select `livres`.*, `auteur_livre`.`auteur_id` as `pivot_auteur_id`, `auteur_livre`.`livre_id` as `pivot_livre_id` from `livres` inner join `auteur_livre` on `livres`.`id` = `auteur_livre`.`livre_id` where `auteur_livre`.`auteur_id` = '4'

De la même manière on peut récupérer le nom des auteurs d'un livre :

1. Livre::find(4)->auteurs->each(function($auteur)

2. {

3. echo $auteur->nom, '<br>';

4. });

Ici j'obtiens :

Auteur 1
Auteur 13

XXXIII-E - Relation polymorphique de type 1:n

Maintenant que vous êtes chauds on peut aborder un sujet un peu plus délicat. Regardez cette partie de la base d'exemples :

On retrouve notre table livres déjà vue pour une autre relation. On a aussi deux autres tables : editeurs et autoedites. Voici ce que l'on veut :

1. Un éditeur peut correspondre à plusieurs livres mais un livre ne peut appartenir qu'à un éditeur ;

2. Un autoéditeur peut correspondre à plusieurs livres mais un livre ne peut appartenir qu'à un autoéditeur :

3. Un livre appartient soit à un éditeur, soit à un autoéditeur, mais pas aux deux.

On se rend compte qu'on aurait du mal à s'en sortir avec deux relations 1:n comme on l'a fait précédemment. On a deux relations de même type qui s'adressent à la même table. Ces deux relations ont la même structure mais changent dans leur forme, d'où l'appellation de « polymorphique » qui veut tout simplement dire « plusieurs formes ».

Dans une relation classique 1:n, on établit le lien entre les tables avec une clé étrangère dans la table du côté n et ça suffit pour s'y retrouver sans ambiguïté. Mais là comment va-t-on faire ? Il nous faut deux renseignements :

1. Le nom de la table en relation ou de son modèle ;

2. L'id de l'enregistrement concerné.

Regardez dans la table des livres, vous trouvez deux champs :

1. livrable_id qui est chargé de contenir l'id de l'enregistrement lié ;

2. livrable_type qui est chargé de contenir le nom du modèle de la table en relation.

Si on sait quelle est la table reliée et qu'on connaît aussi l'id de l'enregistrement, alors la relation est parfaitement connue. Voici une visualisation de la relation :

XXXIII-E-1 - morphTo

Eloquent propose la méthode morphTo pour établir une relation polymorphique à partir de la table qui est du côté n. Il faut donc prévoir cette méthode dans le modèle Livre :

1. public function livrable()

2. {

3. return $this->morphTo();

4. }

Voici la signature de la méthode morphTo :

public function morphTo($name = null, $type = null, $id = null)

Voyons les paramètres :

1. C'est le nom de la relation, et si on n'en donne pas c'est le nom de la méthode qui est utilisé ;

2. C'est le nom du champ qui récupère le type du modèle en liaison, mais si on n'en donne pas il est nommé à partir du nom de la relation auquel on ajoute « _type » ; dans notre cas on a déjà livrable_type ;

3. C'est le nom du champ qui récupère la clé étrangère, et si on n'en donne pas il est nommé à partir du nom de la relation auquel on ajoute « _id », dans notre cas on a déjà livrable_id.

Bon maintenant vous vous demandez peut-être pourquoi se donner autant de peine ? Regardez ce code :

1. Livre::take(5)->get()->each(function($livre)

2. {

3. echo $livre->livrable->nom, '<br>';

4. });

Je prends les cinq premiers livres et dans une boucle je récupère le nom dans la table en relation. La méthode livrable va créer une instance du modèle correspondant, soit Editeur, soit Autoedite selon que le livre est en relation avec l'un ou l'autre. Voilà ce que j'obtiens :

1. Editeur 4

2. Editeur 4

3. Editeur 11

4. Autoedite 2

5. Autoedite 18

XXXIII-E-2 - morphMany

Voyons à présent la relation inverse avec morphMany. Par exemple pour les éditeurs, on doit ajouter la méthode dans le modèle :

1. public function livres()

2. {

3. return $this->morphMany('Livre', 'livrable');

4. }

Voici la signature de cette méthode :

public function morphMany($related, $name, $type = null, $id = null, $localKey = null)

Voyons les paramètres :

1. C'est le nom du modèle de la table ciblée, dans notre cas Livre ;

2. C'est le nom de la relation, dans notre cas livrable ;

3. C'est le nom du champ qui récupère le type du modèle en liaison, et si on n'en donne pas il est nommé à partir du nom de la relation auquel on ajoute « _type » ; dans notre cas on a déjà livrable_type ;

4. C'est le nom du champ qui récupère la clé étrangère, et si on n'en donne pas il est nommé à partir du nom de la relation auquel on ajoute « _id » ; dans notre cas on a déjà livrable_id ;

5. C'est le nom de l'id de la table de départ, et dans notre cas on a « id », valeur par défaut.

Pour le fonctionnement c'est exactement comme un hasMany. On peut par exemple trouver les livres d'un éditeur avec ce code :

1. Editeur::find(4)->livres->each(function($livre)

2. {

3. echo $livre->titre, '<br>';

4. });

Dans mon cas j'obtiens ça :

Titre 1
Titre 2
Titre 26

XXXIII-F - Relation polymorphique de type n:n

Voyons à présent la situation la plus complexe, qui est apparue avec la version 4.1 de Laravel, avec les relations polymorphiques de type n:n. Ce que nous avons vu précédemment devrait vous aider à comprendre de quoi il s'agit. Regardez cette partie de la base d'exemple :

On a une table themes et deux tables : categories et periodes. On veut relier la table des thèmes aux deux autres tables avec ces possibilités :

1. Une catégorie peut avoir plusieurs thèmes ;

2. Une période peut avoir plusieurs thèmes ;

3. Un thème peut avoir plusieurs catégories et plusieurs thèmes.

On est donc dans le cadre d'une relation de type n:n. On a vu précédemment qu'on résout ce genre de situation avec une table pivot. Ici cette table pivot est themables. Dans une relation classique n:n on a vu que cette table pivot contient les clés des deux tables en relation. Ici nous n'avons pas deux tables mais trois. On peut évidemment s'en sortir en créant deux tables pivot. La solution polymorphique est plus élégante. Il y a eu une discussion intéressante sur le sujet lors de sa soumission initiale. Voici comment c'est mis en œuvre :

- du côté de la table des thèmes on n'a pas de souci parce qu'on a une seule table, donc on peut mettre dans la table pivot la clé theme_id ;

- du côté des tables categories et periodes, on adopte ce qu'on a vu pour la relation polymorphique précédente avec deux champs : themable_id pour la clé soit d'une catégorie, soit d'une période, et themable_type pour le nom du modèle de la table en liaison : Categorie ou Periode.

Voici une visualisation de ce que nous allons mettre en place :

XXXIII-F-1 - morphToMany

Du côté des deux tables on utilise la méthode morphToMany. Dans le modèle Categorie :

1. public function themes()

2. {

3. return $this->morphToMany('Theme', 'themable');

4. }

Et dans le modèle Periode :

1. public function themes()

2. {

3. return $this->morphToMany('Theme', 'themable');

4. }

Voici la signature de la méthode morphToMany :

public function morphToMany($related, $name, $table = null, $foreignKey = null, $otherKey = null, $inverse = false)

Pas moins que six paramètres :

1. C'est le nom du modèle de la table ciblée, dans notre cas Theme ;

2. C'est le nom de la relation, dans notre cas themable ;

3. C'est le nom de la table pivot construit à partir du deuxième paramètre auquel on ajoute « s », dans notre cas on a déjà themables ;

4. C'est le nom du champ qui récupère la clé étrangère ; si on n'en donne pas il est nommé à partir du nom de la relation auquel on ajoute « _id », dans notre cas on a déjà themable_id ;

5. C'est le nom du champ qui récupère le type du modèle en liaison, si on n'en donne pas il est nommé à partir du nom de la relation auquel on ajoute « _type », dans notre cas on a déjà themable_type ;

6. ce dernier paramètre m'intrigue, je ne l'ai pas encore vraiment compris donc il méritera des tests complémentaires…

Je peux maintenant trouver tous les thèmes pour une catégorie :

1. Categorie::find(2)->themes->each(function($theme)

2. {

3. echo $theme->nom, '<br>';

4. });

Ou pour une période :

1. Periode::find(2)->themes->each(function($theme)

2. {

3. echo $theme->nom, '<br>';

4. });

XXXIII-F-2 - morphedByMany

Voyons maintenant la relation inverse morphedByMany. On se place cette fois du côté de la table des thèmes. On doit déclarer une méthode par table reliée :

1. public function categories()

2. {

3. return $this->morphedByMany('Categorie', 'themable');

4. }

6. public function periodes()

7. {

8. return $this->morphedByMany('Periode', 'themable');

9. }

La méthode morphedByMany a cette signature :

public function morphedByMany($related, $name, $table = null, $foreignKey = null, $otherKey = null)

Je ne commente pas ces paramètres qui sont les mêmes que ceux de la méthode morphToMany. On peut alors trouver par exemple toutes les catégories pour un thème :

1. Theme::find(2)->categories->each(function($categorie)

2. {

3. echo $categorie->nom, '<br>';

4. });

Avec cette relation polymorphique s'achève ce tour d'horizon des relations traitées par Eloquent. Il existe aussi la méthode morphOne pour les relations de type 1:1 mais franchement c'est anecdotique étant donné la rareté de la chose ; elle n'est d'ailleurs même pas citée dans la documentation.

J'aborderai dans un prochain chapitre la gestion de tout ça. En effet quand on crée des relations, ça a évidemment une conséquence sur la manipulation des enregistrements dans les tables et ce n'est pas toujours rose .

XXXIV - Chapitre 34 : Les relations avec Eloquent 2/2

Modification le 5/5/2014 : quelques changements dans l'organisation du code et dans la syntaxe (en particulier si vous aviez des soucis avec le PSR-0 ça devrait maintenant être réglé).

Dans le précédent chapitreprécédent chapitrechapitre j'ai détaillé les possibilités relationnelles d'Eloquent. Maintenant il nous reste à voir comment gérer tout ça. Le voyage va être parfois un peu mouvementé, alors accrochez-vous…

XXXIV-A - La base

La base de référence qui va nous servir est la même que celle que nous avons vue précédemment :

XXXIV-B - Le code

XXXIV-B-1 - Installation

Comme le code est volumineux je ne vais pas le mettre complètement ici mais juste m'y référer. Vous pouvez le télécharger ici. Il vous suffit de caser tout ça dans un dossier et de faire :

- composer install ;

- créer une base et renseigner son nom dans app/config/database.php ;

- php artisan migrate:install ;

- php artisan migrate ;

- php artisan db:seed.

Vous devriez alors avoir une application fonctionnelle et arriver sur cette page :

XXXIV-B-2 - Organisation du code

Voici l'architecture des dossiers dans app :

Vous pouvez remarquer la présence du dossier Lib qui contient pratiquement tout le code de l'application et le fichier macro.php qui contient des macros HTML et Form. Vous pouvez aussi voir qu'il n'y a pas les dossiers models, views et controllers. Je suis parti sur une organisation du code fondée sur des entités, en l'occurrence les tables. Si vous regardez le contenu du dossier Lib :

Vous voyez un dossier pour presque chacune des tables de la base. Le code correspondant à la gestion de chaque table se trouve dans ce dossier. Par exemple pour les villes :

Vous trouvez ici les vues, le modèle, le contrôleur, la validation et la gestion. c'est la même chose pour chaque table.

Il y a aussi un dossier Commun qui contient tout le code qui concerne toutes les gestions :

Ici on a les templates, la validation (la classe utilisée est celle du bouquin de Fidao Implementing Laravel), les vues communes, la classe abstraite des contrôleurs.

Cette organisation me paraît plus cohérente quand on commence à avoir pas mal de code. Il m'a fallu évidemment expliquer à Laravel où trouver tout ce code. Je l'ai fait au niveau de Composer :

"psr-0": {
"Lib": "app"
}

Pour les vues il m'a fallu changer le path dans app/config/view.php :

'paths' => array(__DIR__.'/../Lib'),

XXXIV-B-3 - Les macros

Pour simplifier les vues avec l'utilisation de bootstrap j'ai créé quelques macros situées dans le fichier app/macros.php. J'ai renseigné le fichier app/start/global.php :

require app_path().'/macros.php';

Par exemple pour faire apparaître une zone de texte avec son étiquette version bootstrap :

1. Form::macro('boottext', function($name, $label, $input = '')

2. {

3. return sprintf('

4. <div class="row">

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

6. %s

7. <div class="col-md-10">

8. %s

9. </div>

10. </div>

11. </div>',

12. Form::label($name, $label, array("class" => "col-md-2")),

13. Form::text($name, $input, array('class' => 'form-control'))

14. );

15. });

Ce qui génère en une seule ligne ce genre de code :

1. <div class="row">

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

3. <label for="nom" class="col-md-2">Nom :</label>

4. <div class="col-md-10">

5. <input class="form-control" name="nom" type="text" value="Pays 5" id="nom">

6. </div>

7. </div>

8. </div>

XXXIV-B-4 - Les routes

J'ai traité tous les contrôleurs comme des ressources, du coup les routes sont très épurées :

1. Route::get('/', function() { return View::make('Commun.vues.accueil'); });

3. Route::resource('villes', 'Lib\Villes\VilleController');

4. Route::resource('pays', 'Lib\Pays\PaysController');

5. Route::resource('auteurs', 'Lib\Auteurs\AuteurController');

6. Route::resource('livres', 'Lib\Livres\LivreController');

7. Route::resource('editeurs', 'Lib\Editeurs\EditeurController');

8. Route::resource('autoedites', 'Lib\Autoedites\AutoediteController');

9. Route::resource('themes', 'Lib\Themes\ThemeController');

10. Route::resource('categories', 'Lib\Categories\CategorieController');

11. Route::resource('periodes', 'Lib\Periodes\PeriodeController');

XXXIV-B-5 - Les contrôleurs

Comme le traitement dans le cas des contrôleurs est similaire à celui des ressources, j'ai créé une classe abstraite qui contient l'essentiel du code :

1. <?php namespace Lib\Commun;

3. use Illuminate\Support\Facades\View;

4. use Illuminate\Support\Facades\Redirect;

6. abstract class BaseResourceController extends \Illuminate\Routing\Controller {

8. protected $gestion;

9. protected $base;

10. protected $message_store;

11. protected $message_update;

13. public function __construct()

14. {

15. $this->beforeFilter('csrf', array('on' => array('post', 'delete', 'put')));

16. $this->beforeFilter('ajax', array('on' => array('delete', 'put')));

17. }

19. public function index()

20. {

21. $lignes = $this->gestion->listePages(10);

22. return View::make($this->base.'.vues.liste', compact('lignes'));

23. }

25. public function create()

26. {

27. return View::make($this->base.'.vues.create',  $this->gestion->create());

28. }

30. public function store()

31. {

32. $return = $this->gestion->store();

33. if($return === true) {

34. return Redirect::route($this->base.'.index')->with('message_success', $this->message_store);

35. }

36. return Redirect::route($this->base.'.create')->withInput()->withErrors($return);

37. }

39. public function show($id)

40. {

41. return View::make($this->base.'.vues.show', $this->gestion->show($id));

42. }

44. public function edit($id)

45. {

46. return View::make($this->base.'.vues.edit', $this->gestion->edit($id));

47. }

49. public function update($id)

50. {

51. $return = $this->gestion->update($id);

52. if($return === true) {

53. return Redirect::route($this->base.'.index')->with('message_success', $this->message_update);

54. }

55. return Redirect::route($this->base.'.edit', $id)->withInput()->withErrors($return);

56. }

58. public function destroy($id)

59. {

60. $this->gestion->destroy($id);

61. return Redirect::back();

62. }

64. }

On se retrouve ainsi avec des contrôleurs très allégés.

XXXIV-B-6 - Considérations générales

J'ai simplifié au maximum la situation pour me concentrer sur les procédures. Chaque table a un seul champ, je ne fais pas de tri pour l'affichage, etc.

J'ai prévu une barre de débogage pour voir les requêtes générées par Eloquent.

XXXIV-C - Gestion des pays

On va commencer avec un cas simple, celui des pays. Au niveau des relations la table des pays est reliée à la table des villes par un lien 1:n :

On a vu également qu'on peut atteindre les auteurs avec la méthode hasManyThrough.

XXXIV-C-1 - Le modèle

Comme on a des espaces de noms, il faut évidemment bien renseigner le code en conséquence pour qu'Eloquent s'y retrouve :

1. <?php namespace Lib\Pays;

3. use Eloquent;

5. class Pays extends Eloquent {

7. protected $table = 'pays';

8. public $timestamps = true;

9. protected $softDelete = false;

10. protected $guarded = array('id');

12. public function villes()

13. {

14. return $this->hasMany('\Lib\Villes\Ville');

15. }

17. public function auteurs()

18. {

19. return $this->hasManyThrough('\Lib\Auteurs\Auteur', '\Lib\Villes\Ville');

20. }

22. }

Ce sera la même chose pour tous les modèles de l'application.

XXXIV-C-2 - Le contrôleur

J'ai parlé plus haut de la classe abstraite pour les contrôleurs, et ça se traduit pour celui des pays à très peu de code :

1. <?php namespace Lib\Pays;

3. class PaysController extends \Lib\Commun\BaseResourceController {

5. public function __construct(PaysGestion $gestion)

6. {

7. parent::__construct();

8. $this->gestion = $gestion;

9. $this->base = class_basename(__NAMESPACE__);

10. $this->message_store = 'Le pays a été ajouté';

11. $this->message_update = 'Le pays a été modifié';

12. }

14. }

La gestion est déléguée à la classe PaysGestion qui est injectée dans le modèle. On a ainsi une bonne séparation des tâches.

XXXIV-C-3 - La liste

Pour obtenir la liste des pays on passe par la méthode ListePages de la classe abstraite BaseGestion commune à toutes les gestions :

1. public function listePages($pages)

2. {

3. return $this->model->paginate($pages);

4. }

Cette méthode est appelée par la méthode index du contrôleur de base :

1. public function index()

2. {

3. $lignes = $this->gestion->listePages(10);

4. return View::make($this->base.'.vues.liste', compact('lignes'));

5. }

On voit ici qu'on appelle la vue liste pour la table concernée, ici celle des pays. Le code des vues pour les listes est similaire pour toutes les tables, voici ce que ça donne pour les pays :

1. @extends('Commun.templates.liste')

3. @section('entete')

4. Liste des pays

5. {{ link_to_route('pays.create', 'Ajouter un pays', null, array('class' => 'btn btn-info pull-right')) }}

6. @stop

8. @section('titre')

9. Pays

10. @stop

12. @section('tableau')

13. @foreach ($lignes as $ligne)

14. <tr>

15. <td>{{ $ligne->id }}</td>

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

17. <td>{{ link_to_route('pays.show', 'Voir', array($ligne->id), array('class' => 'btn btn-success btn-block')) }}</td>

18. <td>{{ link_to_route('pays.edit', 'Modifier', array($ligne->id), array('class' => 'btn btn-warning btn-block')) }}</td>

19. <td>

20. {{ Form::open(array('method' => 'DELETE', 'route' => array('pays.destroy', $ligne->id))) }}

21. {{ Form::submit('Supprimer', array('class' => 'btn btn-danger btn-block', 'onclick' => 'return confirm(\'Vraiment supprimer cet enregoistrement ?\')')) }}

22. {{ Form::close() }}

23. </td>

24. </tr>

25. @endforeach

26. @stop

Je ne rentre pas dans le détail de ce code ni dans le template correspondant parce que ce n'est pas le sujet. Voici l'aspect de la page obtenue :

La pagination est réglée à dix enregistrements. Pour chaque ligne on peut voir le pays, le modifier ou le supprimer.

XXXIV-C-4 - La fiche du pays

Pour voir un pays c'est la méthode show de la gestion qui est appelée :

1. public function show($id)

2. {

3. $pays = $this->model->find($id);

4. return array(

5. 'pays' => $pays,

6. 'villes' => $pays->villes,

7. 'auteurs' => $pays->auteurs

8. );

9. }

La méthode find permet de récupérer l'enregistrement à partir de son ID qui a été renseigné dans la vue. Ensuite on prévoit les informations nécessaires pour la fiche du pays :

- évidemment les champs du pays $pays ;

- les villes du pays récupérées avec $pays->villes (méthode hasMany du modèle) ;

- les auteurs du pays récupérés avec $pays->auteurs(méthode hasManyThrough du modèle).

La vue récupère ces informations :

1. @extends ('Commun.templates.fiche')

3. @section('titre')

4. <h1>Fiche de pays</h1>

5. @stop

7. @section('contenu')

8. {{ HTML::bootpanel('Nom du  pays', $pays->nom) }}

9. {{ HTML::bootpanelmulti('Villes', $villes, 'nom') }}

10. {{ HTML::bootpanelmulti('Auteurs', $auteurs, 'nom') }}

11. @stop

L'utilisation de macros (situées dans le fichier app/macros.php) permet de rendre le code simple et lisible. Résultat obtenu :

XXXIV-C-5 - Modifier un pays

La modification d'un pays est simple parce que ça n'impacte pas la relation avec les villes. Au niveau de la gestion on se contente de récupérer l'enregistrement :

1. public function edit($id)

2. {

3. return array(

4. 'pays' => $this->model->find($id),

5. );

6. }

Et dans la vue on affiche le seul champ disponible (toujours avec les macros) :

1. @extends ('Commun.templates.form')

3. @section('formulaire')

4. {{ Form::open(array('url' => 'pays/'.$pays->id, 'method' => 'put', 'class' => 'form-horizontal panel')) }}

5. @include ('commun.templates.messages')

6. {{ Form::bootlegend('Modification du pays') }}

7. {{ Form::boottext('nom', 'Nom :', $pays->nom) }}

8. {{ Form::bootbuttons(url('pays')) }}

9. {{ Form::close() }}

10. @stop

Ce qui donne cet aspect :

Au retour on teste la validité, on sauvegarde avec la méthode save, et on retourne :

1. public function update($id)

2. {

3. if($this->validation->with(Input::all())->passes())    {

4. $pays = $this->model->find($id);

5. $pays->nom = Input::get('nom');

6. return $pays->save();

7. }

8. return $this->validation->errors();

9. }

La redirection a lieu dans le contrôleur de base :

1. public function update($id)

2. {

3. $return = $this->gestion->update($id);

4. if($return === true) {

5. return Redirect::route($this->base.'.index')->with('message_success', $this->message_update);

6. }

7. return Redirect::route($this->base.'.edit', $id)->withInput()->withErrors($return);

8. }

La création est calquée sur ce modèle, je ne la présente donc pas.

XXXIV-C-6 - Détruire un pays

N'ayez pas peur, rien de belliqueux là-dedans. Étant donné qu'il y a dans la table des villes une clé étrangère avec l'identifiant du pays (en fait autant de clés que de pays en relation), on ne va pas supprimer un pays sans précaution, au risque d'avoir une clé étrangère qui ne se réfère plus à rien du tout. Un petit schéma pour bien voir ça :

Si je ne veux pas que le champ pays_id dans la table villes devienne orpheline, j'ai le choix :

- je peux demander à MySQL (ou autre) de faire une suppression en cascade. Dans ce cas si je supprime le pays, les villes sont aussi automatiquement supprimées dans la base (et ainsi de suite s'il y a une autre cascade). C'est la méthode radicale qu'en général on évite ;

- je peux demander à MySQL de m'interdire la suppression du pays. Du coup, si je le fais je reçois une erreur de sa part. C'est l'attitude par défaut, et c'est celle que j'ai laissée.

Du coup dans le traitement de la suppression je vais vérifier s'il existe encore au moins une ville en relation :

1. public function destroy($id)

2. {

3. $pays = $this->model->find($id);

4. if($pays->villes->count() == 0)

5. {

6. $pays->delete();

7. } else {

8. Session::flash('message_danger', 'Ce pays ne peut pas être supprimé parce qu\'il possède des villes !');

9. }

10. }

S'il y a au moins une ville j'envoie un message, sinon je supprime le pays.

XXXIV-D - Gestion des villes

Voyons à présent la gestion des villes. Il y a pas mal de similitude avec celle des pays, je vais donc m'attacher à montrer les particularités.

Au niveau des relations on a une double liaison :

Un ville appartient à un pays (relation 1:n vue du côté n) et une ville possède plusieurs auteurs (relation 1:n vue du côté 1). Qu'est-ce que cela implique ?

Pour l'affichage de la liste il n'y a aucune différence avec les pays. Pour la suppression c'est pareil, on va faire attention parce que la table auteurs possède la clé étrangère ville_id. Je ne reviens donc pas sur ces deux points, il suffit de vous reporter à la gestion des pays vue ci-dessus.

XXXIV-D-1 - La fiche de la ville

Dans la fiche de la ville on va afficher le pays auquel elle appartient et tous les auteurs qui l'habitent. Ce qui donne ce code de gestion :

1. public function show($id)

2. {

3. $ville = $this->model->find($id);

4. return array(

5. 'ville' => $ville,

6. 'pays' => $ville->pays->nom,

7. 'auteurs' => $ville->auteurs

8. );

9. }

Et cette vue :

1. @extends ('Commun.templates.fiche')

3. @section('titre')

4. <h1>Fiche de ville</h1>

5. @stop

7. @section('contenu')

8. {{ HTML::bootpanel('Nom de la ville', $ville->nom) }}

9. {{ HTML::bootpanel('Nom du pays', $pays) }}

10. {{ HTML::bootpanelmulti('Auteurs', $auteurs, 'nom') }}

11. @stop

Et cet aspect :

XXXIV-D-2 - Modifier une ville

La table ville comporte deux champs :

- nom : c'est le nom de la ville ;

- pays_id : c'est la clé étrangère qui se réfère au pays.

Il faut donc permettre de modifier ces deux valeurs. Pour le nom c'est simple, un simple contrôle de texte fait l'affaire. Pour le pays c'est plus délicat, il faut proposer un choix parmi tous les pays, donc prévoir une liste de choix avec tous les noms de pays. Voici la gestion :

1. public function edit($id)

2. {

3. $ville = $this->model->find($id);

4. return array(

5. 'ville' => $ville,

6. 'select' => $this->pays->all()->lists('nom', 'id')

7. );

8. }

La variable $ville contiendra les informations de la table villes et la variable $select contiendra tous les pays avec leur nom et leur id. Avec les macros la vue est épurée :

1. @extends ('Commun.templates.form')

3. @section('formulaire')

4. {{ Form::open(array('url' => 'villes/'.$ville->id, 'method' => 'put', 'class' => 'form-horizontal panel')) }}

5. @include ('commun.templates.messages')

6. {{ Form::bootlegend('Modification de la ville') }}

7. {{ Form::boottext('nom', 'Nom :', $ville->nom) }}

8. {{ Form::bootselect('pays_id', 'Pays :', $select, $ville->pays_id) }}

9. {{ Form::bootbuttons(url('villes')) }}

10. {{ Form::close() }}

11. @stop

On obtient ce formulaire avec le nom de la ville et la liste de choix avec le bon pays sélectionné :

Au retour on teste la validité et on enregistre :

1. public function update($id)

2. {

3. if($this->validation->with(Input::all())->passes())

4. {

5. $ville = $this->model->find($id);

6. $ville->nom = Input::get('nom');

7. $ville->pays_id = Input::get('pays_id');

8. return $ville->save();

9. }

10. return $this->validation->errors();

11. }

XXXIV-D-3 - Créer une ville

Pour créer une ville c'est pratiquement la même chose pour le formulaire, à part que le nom est vierge et qu'au retour il faut créer une nouvelle entité :

1. public function store()

2. {

3. if($this->validation->with(Input::all())->passes())    {

4. $ville = new $this->model(Input::all());

5. return $ville->save();

6. }

7. return $this->validation->errors();

8. }

XXXIV-E - Gestion des livres

Avec la gestion des livres on attaque un gros morceau. Voici les trois relations :

- avec les éditeurs et les autoéditeurs on a une relation polymorphique de type 1:n vue du côté n ;

- avec les thèmes on a une relation de type 1:n vue du côté n ;

- avec les auteurs on a une relation de type n:n.

Nous avons déjà vu le deuxième cas avec les villes, le traitement sera donc identique. Par contre nous avons deux nouveaux cas à analyser attentivement.

Pour la relation avec les auteurs de type n:n on sait qu'il nous faut une table pivot, ici c'est auteur_livre. On part du principe qu'un livre a au moins un auteur et une quantité maximale indéterminée d'auteurs.

Pour la relation polymorphique on sait qu'un livre appartient soit à un éditeur, soit à un autoéditeur.

XXXIV-E-1 - La fiche du livre

La fiche du livre doit comporter :

- le titre du livre ;

- le thème ;

- le nom de l'éditeur ou de l'autoéditeur ;

- les noms des auteurs.

Voici la méthode show :

1. public function show($id)

2. {

3. $livre = $this->model->find($id);

4. return array(

5. 'livre' => $livre,

6. 'auteurs' => $livre->auteurs,

7. 'theme' => $livre->theme->nom,

8. 'editeur' => $livre->livrable->nom

9. );

10. }

On a quatre variables transmises pour les quatre informations nécessaires. Voici la vue :

1. @extends ('Commun.templates.fiche')

3. @section('titre')

4. <h1>Fiche de livre</h1>

5. @stop

7. @section('contenu')

8. {{ HTML::bootpanel('Titre du livre', $livre->titre) }}

9. {{ HTML::bootpanel('Thème', $theme) }}

10. {{ HTML::bootpanel('Editeur', $editeur) }}

11. {{ HTML::bootpanelmulti('Auteurs', $auteurs, 'nom') }}

12. @stop

Et le résultat :

Vous remarquez comment Eloquent permet de faire ça avec simplicité.

XXXIV-E-2 - Modifier un livre

Maintenant envisageons la modification d'un livre. Cette fois je vais partir à l'envers. On veut le formulaire suivant :

Avec ces possibilités :

- pour le titre : un simple contrôle de texte ;

- pour le thème : une liste de choix avec tous les thèmes et au départ le bon thème sélectionné ;

- pour les auteurs : au départ autant de listes de choix que d'auteurs avec le bon auteur chaque fois sélectionné, un bouton de suppression pour chaque auteur, et un bouton pour ajouter un auteur, on aura donc là un traitement dynamique du formulaire ;

- pour les éditeurs : deux boutons « radio » pour choisir entre les deux possibilités avec le bon bouton sélectionné au départ, et une liste de choix avec - selon le bouton radio sélectionné - les éditeurs ou les autoéditeurs, avec évidemment le bon sélectionné au départ.

Voici la gestion correspondante :

1. public function edit($id)

2. {

3. $old = Input::old();

4. $livre = $this->model->find($id);

5. if(empty($old))

6. {

7. $livrable = $livre->livrable_type == 'Lib\Editeurs\Editeur';

8. } else {

9. $livrable = $old['livrable'] == 'Lib\Editeurs\Editeur';

10. }

11. $retour = array(

12. 'livre' => $livre,

13. 'select_theme' => $this->theme->all()->lists('nom', 'id'),

14. 'select_auteurs' => $this->auteur->all()->lists('nom', 'id'),

15. 'select_editeurs' => $this->editeur->all()->lists('nom', 'id'),

16. 'select_autoedites' => $this->autoedite->all()->lists('nom', 'id'),

17. 'livrable' => $livrable

18. );

19. $retour['auteurs'] = empty($old) ? $livre->auteurs->toArray(): $old['auteur'];

20. return $retour;

21. }

On va regarder tout ça de près. Déjà au niveau des informations transmises :

- pour tous les champs du livre c'est tout simple, on les a directement dans le modèle ;

- pour remplir les listes des thèmes, des auteurs, des éditeurs et des autoéditeurs, on envoie les quatre paquets avec :

1. 'select_theme' => $this->theme->all()->lists('nom', 'id'),

2. 'select_auteurs' => $this->auteur->all()->lists('nom', 'id'),

3. 'select_editeurs' => $this->editeur->all()->lists('nom', 'id'),

4. 'select_autoedites' => $this->autoedite->all()->lists('nom', 'id'),

- pour savoir si c'est éditeur ou autoéditeur il y a plusieurs façons de faire. J'ai choisi de transmettre une valeur booléenne qui indique qu'il s'agit des éditeurs si elle est vraie, et évidemment l'inverse si elle est fausse ;

- pour les auteurs, on les trouve facilement avec la relation $livre->auteurs.

Le traitement spécifique des auteurs et des éditeurs est nécessaire en cas d'échec de validation. Il faut dans ce cas renvoyer à l'utilisateur ses choix en générant les bons contrôles. C'est la raison du test de présence d'anciennes entrées, qui servent de référence dans ce cas.

La vue est évidemment un peu chargée pour traiter tout ça :

1. @extends ('Commun.templates.form')

3. @section('formulaire')

4. {{ Form::open(array('url' => 'livres/'.$livre->id, 'method' => 'put', 'class' => 'form-horizontal panel')) }}

5. @include ('commun.templates.messages')

6. {{ Form::bootlegend('Modification du livre') }}<br>

7. {{ Form::boottext('titre', 'Titre :', $livre->titre) }}<br>

8. {{ Form::bootselect('theme_id', 'Theme :', $select_theme, $livre->theme_id) }}<br>

9. @for ($i = 0; $i < count($auteurs); $i++)

10. {{ Form::bootselectbutton('auteur', $i, 'Auteur :', $select_auteurs, $auteurs[$i]) }}

11. @endfor

12. <div class="row">

13. <button id="add" type="button" class="btn btn-primary pull-right">Ajouter un auteur</button>

14. </div>

15. <br>

16. <div class="row form-group">

17. <label class="radio-inline">

18. {{ Form::radio('livrable', 'Lib\Editeurs\Editeur', $livrable) }} Editeur

19. </label>

20. <label class="radio-inline">

21. {{ Form::radio('livrable', 'Lib\Autoedites\Autoedite', !$livrable) }} Auto Editeur

22. </label>

23. </div>

24. <div class="toggle {{ $livrable? 'show' : 'hidden' }}">

25. {{ Form::bootselect('editeur_id', 'Editeur :', $select_editeurs, $livre->livrable_id) }}

26. </div>

27. <div class="toggle {{ $livrable? 'hidden' : 'show' }}">

28. {{ Form::bootselect('autoedite_id', 'Auto Editeur :', $select_autoedites, $livre->livrable_id) }}

29. </div>

30. <br><hr>

31. {{ Form::bootbuttons(url('livres')) }}

32. {{ Form::close() }}

33. @stop

35. @section('scripts')

36. <script>

37. $(function(){

38. // Suppression d'une ligne d'auteurs

39. $(".btn-danger").click(function() {

40. // On supprime la ligne s'il en reste au moins 2

41. if($('.ligne').length > 1) $(this).parents('.row .ligne').remove();

42. });

44. // Ajout d'une ligne d'auteurs

45. $("#add").click(function() {

46. // Recherche dernier id

47. var max = id = 0;

48. $('.ligne').each(function(){

49. id = parseInt(($(this).attr('id')).substring(11));

50. if(id > max) max = id;

51. });

52. // Première ligne

53. var clone = $('#ligneauteur' + max).clone(true);

54. // Change l'id

55. clone.attr('id', 'ligneauteur' + ++max);

56. // Change le for du label

57. clone.find('label').attr('for', 'auteur' + max);

58. // Change l'id du select

59. clone.find('select').attr('id', 'auteur' + max);

60. // Ajoute la ligne à la fin

61. $('#ligneauteur' + id).after(clone);

62. });

64. // Changement editeur/auto editeur

65. $('input[type="radio"]').change(function () {

66. $('.toggle').toggleClass('show hidden');

67. });

69. })

70. </script>

71. @stop

Je ne détaille pas tout ce code et il est sans doute améliorable, je n'ai pas cherché à l'optimiser. Je précise juste que je gère l'aspect dynamique avec jQuery, en clonant le contrôle des auteurs puisqu'il doit en rester au moins un, et en jouant avec des classes pour les éditeurs.

Le traitement au retour est assez facile avec Eloquent :

1. public function update($id)

2. {

3. if($this->validation->with(Input::all())->passes())

4. {

5. $livre = $this->model->find($id);

6. DB::transaction(function() use($livre)

7. {

8. $livre->auteurs()->sync(Input::get('auteur'));

9. $livre->titre = Input::get('titre');

10. $livre->theme_id = Input::get('theme_id');

11. $type = Input::get('livrable');

12. $livre->livrable_type = $type;

13. $livre->livrable_id = ($type == 'Lib\Editeurs\Editeur') ? Input::get('editeur_id') : Input::get('autoedite_id');

14. $livre->save();

15. });

16. return true;

17. }

18. return $this->validation->errors();

19. }

Notez la synchronisation élégante de la table pivot auteur_livre avec cette simple ligne de code :

$livre->auteurs()->sync(Input::get('auteur'));

XXXIV-E-3 - Transaction ?

Vous avez sans doute remarqué que j'ai placé le code de la mise à jour du livre dans une fonction anonyme pour la transaction. Mais qu'est-ce qu'une transaction ? Dans la mise à jour du livre on fait deux choses :

- on met à jour la table pivot pour référencer les auteurs ;

- on met à jour la table des livres.

Imaginez que la première action se fasse et pas la seconde. On aurait la moitié de ce que l'on veut qui serait exécuté. Dans le cas présent ça ne pose aucun problème au niveau intégrité de la base, mais ce n'est quand même pas une situation souhaitable parce qu'on ne sait plus où on en est. L'attitude dans ce cas est de dire : on fait tout ou on ne fait rien. C'est l'objet d'une transaction. Le fait d'inclure les deux actions dans la transaction nous assure qu'elles seront soit exécutées toutes les deux, soit aucune des deux, mais pas à moitié.

XXXIV-E-4 - Création d'un livre

Pour créer un livre le formulaire est identique, il est juste non renseigné au départ. Je ne détaille donc pas cette partie du code. La seule chose intéressante à noter est la manière de créer les lignes dans la table pivot. Voici la méthode store :

1. public function store()

2. {

3. if($this->validation->with(Input::all())->passes())

4. {

5. $livre = new $this->model;

6. $livre->theme_id = Input::get('theme_id');

7. $livre->titre = Input::get('titre');

8. $type = Input::get('livrable');

9. $livre->livrable_type = $type;

10. $livre->livrable_id = ($type == 'Lib\Editeurs\Editeur') ? Input::get('editeur_id') : Input::get('autoedite_id');

11. DB::transaction(function() use($livre)

12. {

13. $livre->save();

14. $auteurs = array_unique(Input::get('auteur'));

15. foreach($auteurs as $auteur_id)

16. {

17. $livre->auteurs()->attach($auteur_id);

18. }

19. });

20. return true;

21. }

22. return $this->validation->errors();

23. }

Remarquez l'utilisation de la méthode attach pour ajouter les lignes dans la table pivot. Il faut encore utiliser une transaction parce qu'on pourrait se retrouver avec un livre sans auteur.

XXXIV-E-5 - Destruction d'un livre

Pour détruire un livre il n'y a pas de précaution particulière à prendre, si ce n'est de détruire également les lignes de la table pivot :

1. public function destroy($id)

2. {

3. $livre = $this->model->find($id);

4. DB::transaction(function() use($livre)

5. {

6. $livre->auteurs()->detach();

7. $livre->delete();

8. });

9. }

Cela se fait avec la méthode detach sans paramètre pour les effacer tous. Il faut le faire avant la suppression du livre pour respecter l'intégrité référentielle de la base et ne pas tomber sur une erreur. On aurait pu aussi dire à MySQL de faire une suppression en cascade, mais c'est toujours un peu risqué. On utilise encore une transaction puisque nous avons deux actions.

XXXIV-F - Gestion des éditeurs

On va souffler un peu avec les éditeurs qui ne présentent pas de difficultés, si ce n'est une relation de type 1:1 avec les contacts. et une relation polymorphique avec les livres :

Pour renseigner la fiche des éditeurs il faut aller récupérer le téléphone dans la table de contacts. On va chercher aussi les livres de l'éditeur sélectionné pour les afficher :

1. public function show($id)

2. {

3. $editeur = $this->model->find($id);

4. return array(

5. 'editeur' => $editeur,

6. 'livres' => $editeur->livres,

7. 'contact' => $editeur->contact->telephone

8. );

9. }

De la même façon pour la modification d'un éditeur il faut aller chercher le téléphone :

1. public function edit($id)

2. {

3. $editeur = $this->model->find($id);

4. return array(

5. 'editeur' => $editeur,

6. 'telephone' => $editeur->contact->telephone

7. );

8. }

Au retour on doit mettre à jour les deux tables :

1. public function update($id)

2. {

3. if($this->validation->with(Input::all())->passes())

4. {

5. $editeur = $this->model->find($id);

6. $editeur->nom = Input::get('nom');

7. $editeur->contact->telephone = Input::get('telephone');

8. DB::transaction(function() use($editeur)

9. {

10. $editeur->contact->save();

11. $editeur->save();

12. });

13. return true;

14. }

15. return $this->validation->errors();

16. }

Encore une fois une transaction est nécessaire.

Pour créer un nouvel éditeur on a ce code :

1. public function store()

2. {

3. if($this->validation->with(Input::all())->passes())

4. {

5. $editeur = new $this->model;

6. $editeur->nom = Input::get('nom');

7. $contact = new $this->contact;

8. $contact->telephone = Input::get('telephone');

9. DB::transaction(function() use($editeur, $contact)

10. {

11. $editeur->save();

12. $editeur->contact()->save($contact);

13. });

14. return true;

15. }

16. return $this->validation->errors();

17. }

Remarquez qu'on n'a pas besoin de renseigner la clé étrangère editeur_id, Eloquent s'en charge pour nous.

XXXIV-G - Gestion des thèmes

Voyons à présent la gestion des thèmes. Voyons la situation :

La table des thèmes possède une triple relation (une de type 1:n et deux polymorphiques de type n:n) :

- hasMany avec la table des livres ;

- morphedByMany avec la table des catégories ;

- morphedByMany avec la table des périodes.

Comme on l'a vu dans le précédent chapitre la polymorphie de type n:n remplace plusieurs relations de type n:n. Au lieu d'avoir autant de tables pivots que de relations on en a une seule qui les regroupe. L'identification se fait avec le nom du modèle en relation en plus des deux ID. Un thème peut donc être en relation avec 0 ou plusieurs catégories et avec 0 ou plusieurs périodes.

XXXIV-G-1 - La fiche du thème

Que nous faut-il comme information pour la fiche d'un thème ? Voici la gestion correspondante :

1. public function show($id)

2. {

3. $theme = $this->model->find($id);

4. return array(

5. 'theme' => $theme,

6. 'livres' => $theme->livres,

7. 'categories' => $theme->categories,

8. 'periodes' => $theme->periodes

9. );

10. }

On récupère : le thème sélectionné, les livres, les catégories et les périodes. Au niveau de la vue on procède toujours avec les macros :

1. @extends ('Commun.templates.fiche')

3. @section('titre')

4. <h1>Fiche de thème</h1>

5. @stop

7. @section('contenu')

8. {{ HTML::bootpanel('Nom du thème', $theme->nom) }}

9. {{ HTML::bootpanelmulti('Catégories', $categories, 'nom') }}

10. {{ HTML::bootpanelmulti('Périodes', $periodes, 'nom') }}

11. {{ HTML::bootpanelmulti('Livres', $livres, 'titre') }}

12. @stop

Dans ce cas on a trois panneaux multiples :

XXXIV-G-2 - La modification d'un thème

Voici le formulaire qu'on veut :

Il se divise en trois parties :

- pour le nom un simple contrôle de texte ;

- pour les catégories des listes de choix avec traitement dynamique (de 0 à n listes) ;

- pour les périodes des listes de choix avec traitement dynamique (de 0 à n listes).

Voici le code qui renseigne ce formulaire :

1. public function edit($id)

2. {

3. $theme = $this->model->find($id);

4. $retour = array(

5. 'theme' => $theme,

6. 'select_categories' => $this->categorie->all()->lists('nom', 'id'),

7. 'select_periodes' => $this->periode->all()->lists('nom', 'id')

8. );

9. $old = Input::old();

10. $retour['categories'] = empty($old) ? $theme->categories->toArray(): $old['categorie'];

11. $retour['periodes'] = empty($old) ? $theme->periodes->toArray(): $old['periode'];

12. return $retour;

13. }

On prévoit d'envoyer les champs du thème ($theme) et les éléments de remplissage des listes ($select_categories et $select_periodes). Pour le nombre de listes de choix à afficher, ça dépend évidemment de la situation : envoi initial du formulaire ou nouveau remplissage après erreur de validation. D'où le test de présence d'anciennes données.

La vue est un peu chargée parce qu'elle comporte le traitement dynamique des listes de choix en jQuery :

1. @extends ('Commun.templates.form')

3. @section('formulaire')

4. {{ Form::open(array('url' => 'themes/'.$theme->id, 'method' => 'put', 'class' => 'form-horizontal panel')) }}

5. @include ('commun.templates.messages')

6. {{ Form::bootlegend('Modification du thème') }}

7. {{ Form::boottext('nom', 'Nom :', $theme->nom) }}<br><hr>

8. <div id="cats">

9. @for ($i = 0; $i < count($categories); $i++)

10. {{ Form::bootselectbutton('categorie', $i, 'Catégorie :', $select_categories, $categories[$i]) }}

11. @endfor

12. </div>

13. <div class="row">

14. <button id="add_cat" type="button" class="btn btn-primary pull-right">Ajouter une catégorie</button>

15. </div>

16. <br><hr>

17. <div id="pers">

18. @for ($i = 0; $i < count($periodes); $i++)

19. {{ Form::bootselectbutton('periode', $i, 'Période :', $select_periodes, $periodes[$i]) }}

20. @endfor

21. </div>

22. <div class="row">

23. <button id="add_per" type="button" class="btn btn-primary pull-right">Ajouter une période</button>

24. </div>

25. <br><hr>

26. {{ Form::bootbuttons(url('themes')) }}

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

28. @stop

30. @section('scripts')

31. <script>

33. $(function(){

35. // Nombre de catégories et périodes au départ

36. var cat_number = $('#cats .ligne').length;

37. var per_number = $('#pers .ligne').length;

39. // Suppression d'une ligne

40. $(document).on('click', '.btn-danger', function(){

41. $(this).parents('.row .ligne').remove();

42. });

44. // Ajout d'une ligne de catégorie

45. $("#add_cat").click(function() {

46. var id = 'categorie' + cat_number;

47. var html = '<div class="row ligne" id="ligne' + id + '">\n<div class="form-group">\n'

48. + '<label for="categorie' + cat_number + '" class="col-md-3">Catégorie :</label>\n'

49. + '<div class="col-md-7">\n'

50. + '{{ Form::select('categorie[]', $select_categories, null, array('class' => 'form-control', 'id' => 'id_temp')) }}\n'

51. + '</div>\n<div class="col-md-2">\n<button type="button" class="btn btn-danger">Supprimer</button>\n</div>\n</div>\n';

52. ++cat_number;

53. $('#cats').append(html);

54. $('#id_temp').attr('id', id);

55. });

57. // Ajout d'une ligne de période

58. $("#add_per").click(function() {

59. var id = 'periode' + per_number;

60. var html = '<div class="row ligne" id="ligne' + id + '">\n<div class="form-group">\n'

61. + '<label for="periode' + per_number + '" class="col-md-3">Période :</label>\n'

62. + '<div class="col-md-7">\n'

63. + '{{ Form::select('periode[]', $select_periodes, null, array('class' => 'form-control', 'id' => 'id_temp')) }}\n'

64. + '</div>\n<div class="col-md-2">\n<button type="button" class="btn btn-danger">Supprimer</button>\n</div>\n</div>\n';

65. ++per_number;

66. $('#pers').append(html);

67. $('#id_temp').attr('id', id);

68. });

70. })

72. </script>

73. @stop

Là encore on peut sans doute améliorer ce code mais il accomplit dignement sa tâche. J'ai utilisé la méthode select de Form pour générer une partie du JavaScript. Au retour il faut mettre à jour l'enregistrement :

1. public function update($id)

2. {

3. if($this->validation->with(Input::all())->passes())

4. {

5. $theme = $this->model->find($id);

6. DB::transaction(function() use($theme)

7. {

8. $theme->categories()->sync(Input::get('categorie'));

9. $theme->periodes()->sync(Input::get('periode'));

10. $theme->nom = Input::get('nom');

11. $theme->save();

12. });

13. return true;

14. }

15. return $this->validation->errors();

16. }

J'utilise à nouveau une transaction parce qu'il y a plusieurs actions. La méthode sync permet une mise à jour simplifiée de la table pivot.

XXXIV-G-3 - Créer un thème

Pour créer un thème, le formulaire est le même que pour la mise à jour sans le renseignement initial. On a juste besoin de remplir les listes de choix :

1. public function create(){

2. return array(

3. 'select_categories' => $this->categorie->all()->lists('nom', 'id'),

4. 'select_periodes' => $this->periode->all()->lists('nom', 'id')

5. );

6. }

Au retour il faut créer le thème :

1. public function store()

2. {

3. if($this->validation->with(Input::all())->passes())

4. {

5. $theme = new $this->model;

6. $theme->nom = Input::get('nom');

7. DB::transaction(function() use($theme)

8. {

9. $theme->save();

10. if(!is_null(Input::get('categorie'))) $theme->categories()->attach(array_unique(Input::get('categorie')));

11. if(!is_null(Input::get('periode'))) $theme->periodes()->attach(array_unique(Input::get('periode')));

12. });

13. return true;

14. }

15. return $this->validation->errors();

16. }

Il faut évidemment encore une transaction. La création des lignes dans la table pivot se fait simplement avec la méthode attach.

XXXIV-G-4 - Supprimer un thème

Pour supprimer un thème il faut évidemment supprimer les lignes correspondantes dans la table pivot :

1. public function destroy($id)

2. {

3. $theme = $this->model->find($id);

4. if($theme->livres->count() == 0) {

5. DB::transaction(function() use($theme)

6. {

7. $theme->categories()->detach();

8. $theme->periodes()->detach();

9. $theme->delete();

10. });

11. } else {

12. Session::flash('message_danger', 'Ce thème ne peut pas être supprimé parce qu\'il possède des livres !');

13. }

14. }

On voit que ça se fait facilement avec la méthode detach, qui est l'opposée de la méthode attach vue pour la création. Le fait de ne pas mentionner de paramètre signifie qu'on veut tout supprimer (oui il faut le savoir, ce n'est pas précisé dans la documentation).

XXXV - Chapitre 35 : Les composeurs de vues

Je lisais récemment des articles sur la création de menus avec Laravel. Je n'y ai pas vraiment trouvé quelque chose pour me satisfaire. Alors je me suis penché sur la question. Les solutions possibles sont assez variées. Je me suis orienté vers un composeur de vues. Je vous donne le résultat de ma réflexion.

XXXV-A - Les composeurs de vues

Mais d'abord qu'est-ce qu'un un composeur de vue ? C'est une fonction anonyme ou une classe qui est appelée lorsqu'une vue est créée. Ça permet de préparer des données pour la vue de façon simple et systématique, et de localiser le code à un seul endroit.

Créer un composeur est facile :

1. View::composer('navigation', function($view)

2. {

3. $view->with('menu', Menu::all());

4. });

Avec ce code chaque fois que la vue navigation est créée, elle reçoit la variable $menu avec les informations nécessaires.

On peut aussi utiliser une classe si on a d'autres traitements à effectuer :

View::composer('navigation', 'MenuComposer');

Avec une classe constituée de cette manière :

1. class MenuComposer {

3. public function compose($view)

4. {

5. $view->with('menu', Menu::all());

6. }

7. }

Voilà pour la présentation générale. Venons-en maintenant au menu.

XXXV-B - Format du menu

La première question que je me suis posée est : sous quelle forme est-il judicieux de créer un menu ? Après quelques essais j'en suis venu à adopter le format JSON pour sa versatilité. Il est facile à manipuler autant en PHP qu'en JavaScript, et il est aussi très facile à transmettre.

Voici le menu qui va servir pour notre exemple :

1. {

2. "Accueil" : {

3. "type": "url",

4. "value": "/"

5. },

6. "A propos" : {

7. "A propos de moi" : {

8. "type": "route",

9. "value": "apropos"

10. },

11. "A propos du site" : {

12. "type": "route",

13. "value": "aproposite"

14. }

15. },

16. "Gallerie" : {

17. "type": "url",

18. "value": "gallerie"

19. },

20. "Liens" : {

21. "Quelques liens :" : {

22. "type" : "header"

23. },

24. "Les lieux" : {

25. "type": "route",

26. "value": "lieux"

27. },

28. "Les activités" : {

29. "type": "route",

30. "value": "activites",

31. "state": "disabled"

32. },

33. "divider1" : {

34. "type": "divider"

35. },

36. "Les ressources" : {

37. "type": "route",

38. "value": "ressources"

39. }

40. }

41. }

La structure est simple, on a :

- le nom de l'item ;

- son type (URL, route, header ou divider) ;

- sa valeur éventuelle (pour l'URL ou la route) ;

- son état éventuel (en fait juste s'il est désactivé).

Il suffit de mettre tout ça dans un fichier qu'on va nommer navigation.php. Mais où va-t-on le placer ?

XXXV-C - Organisation du code

Fidèle à mes habitudes j'ai créé un dossier app/Lib :

On va faire le tour de tous ces fichiers. Pour que Laravel connaisse ce dossier et les classes contenues j'en informe Composer :

1. "autoload": {

2. "classmap": [

3. ...

4. ],

5. "psr-0": {

6. "Lib": "app"

7. }

8. },

Après un petit dumpautoload ça devrait aller. Pour que Laravel soit au courant que je positionne les vues dans le dossier app/Lib/views, je le lui dis dans le fichier app/config/views.php :

'paths' => array(__DIR__.'/../Lib/views'),

Le menu créé plus haut trouve sa place dans le dossier Lib/menus.

XXXV-D - Le composeur

Occupons-nous maintenant du composeur (Lib/composers/NavigationComposer.php) :

1. <?php namespace Lib\Composers;

3. use Illuminate\Filesystem\Filesystem;

4. use Illuminate\View\View;

6. class NavigationComposer {

8. protected $files;

10. public function __construct(Filesystem $files)  {

11. $this->files = $files;

12. }

14. public function compose(View $view) {

16. $name = $view->getName();

17. $fileName = app_path().'/Lib/menus/'.$name.'.php';

19. if($this->files->exists($fileName)) {

20. $file = $this->files->get($fileName);

21. $items = json_decode($file, true);

22. $view->items = $items;

23. }

25. }

27. }

Je m'arrange pour que la vue et le menu correspondant aient le même nom. Il me suffit donc de récupérer ce nom et de déterminer le chemin qui y mène. La fonction json_decode permet de transformer le JSON en tableau. Tant qu'à faire j'ai utilisé la classe Filesystem de Laravel pour la manipulation du fichier, plutôt que les fonctions de base de PHP. À la sortie je crée une variable $items pour la vue.

Pour que Laravel soit au courant que ce composeur existe et le relie à la vue, il faut un fournisseur de service (Lib/composers/ComposerServiceProvider.php) :

1. <?php namespace Lib\Composers;

3. use Illuminate\Support\ServiceProvider;

5. class ComposerServiceProvider extends ServiceProvider {

7. public function register()

8. {

9. $this->app->view->composer('navigation', 'Lib\Composers\NavigationComposer');

10. }

12. }

Si j'avais plusieurs menus je les déclarerais tous ici.

Et évidemment il faut déclarer le service dans app/config/app.php :

1. ...

2. 'Illuminate\Workbench\WorkbenchServiceProvider',

3. 'Lib\Composers\ComposerServiceProvider'

4. ),

XXXV-E - Les vues

XXXV-E-1 - Le template

Il ne nous reste plus qu'à nous occuper des vues. D'abord le template (Lib/views/main.blade.php) :

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

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

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

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

7. <title>{{ trans('main.mon_joli_site') }}</title>

8. {{ HTML::style('https://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css') }}

9. {{ HTML::style('https://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap-theme.min.css') }}

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

11. {{ HTML::style('https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js') }}

12. {{ HTML::style('https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js') }}

13. <![endif]-->

14. </head>

15. <body>

16. @include('navigation')

17. <div class="container">

18. @yield('contenu')

19. </div>

20. {{ HTML::script('http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js') }}

21. {{ HTML::script('http://netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js') }}

22. </body>

23. </html>

XXXV-E-2 - La navigation

C'est la vue (Lib/views/navigation.blade.php) qui va utiliser la variable transmise par le composeur :

1. <nav class="navbar navbar-default" role="navigation">

2. <div class="container">

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

4. <a class="navbar-brand">Mon joli site</a>

5. </div>

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

7. @foreach($items as $key => $value)

8. @if(isset($value['type']))

9. <li {{ Request::is($value['value']) ? ' class="active"' : null }} {{ isset($value['state'])? 'class='.$v['state'] : null }} >

10. @if($value['type'] == 'url')

11. <a href="{{ URL::to($value['value']) }}">{{ $key }}</a>

12. @else

13. <a href="{{ URL::route($value['value']) }}">{{ $key }}</a>

14. @endif

15. </li>

16. @else

17. <li class="dropdown">

18. <a class="dropdown-toggle" data-toggle="dropdown" href="#"> {{ $key }} <span class="caret"></span></a>

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

20. @foreach ($value as $k => $v)

21. @if($v['type'] == 'header')

22. <li class="dropdown-header">{{ $k }}</li>

23. @elseif($v['type'] == 'divider')

24. <li class="divider"></li>

25. @else

26. <li {{ Request::is($v['value']) ? ' class="active"' : null }} {{ isset($v['state'])? 'class='.$v['state'] : null }} >

27. @if($v['type'] == 'url')

28. <a href="{{ URL::to($v['value']) }}">{{ $k }}</a>

29. @elseif($v['type'] == 'route')

30. <a href="{{ URL::route($v['value']) }}">{{ $k }}</a>

31. @endif

32. </li>

33. @endif

34. @endforeach

35. </ul>

36. </li>

37. @endif

38. @endforeach

39. </ul>

40. </div>

41. </nav>

Le tableau transmis est parcouru et le menu construit progressivement. J'ai utilisé Bootstrap par facilité (les classes sont toutes prêtes) mais j'aurais pu procéder différemment. Je fais un test de l'URL en cours pour rendre actif l'item correspondant.

XXXV-E-3 - Les autres vues

Les autres vues sont toutes calquées sur le même modèle. Par exemple pour Lib/views/home.blade.php :

1. @extends('main')

3. @section('contenu')

4. Accueil

5. @stop

Pour mes essais j'ai juste changé le texte pour les autres vues.

XXXV-F - Les routes

Pour que tout fonctionne il ne nous manque plus que les routes :

1. Route::get('/', function() { return View::make('home'); });

2. Route::get('apropos', array('as' => 'apropos', function() { return View::make('apropos'); }));

3. Route::get('aproposite', array('as' => 'aproposite', function() { return View::make('aproposite'); }));

4. Route::get('gallerie', function() { return View::make('gallerie'); });

5. Route::get('lieux', array('as' => 'lieux', function() { return View::make('lieux'); }));

6. Route::get('ressources', array('as' => 'ressources', function() { return View::make('ressources'); }));

7. Route::get('activites', array('as' => 'activites', function() { return View::make('activites'); }));

J'ai un peu mélangé les URL et les routes nommées pour les tests.

XXXV-G - Le résultat

Avec l'URL de base j'ai ce résultat :

J'ai bien l'accueil actif et les sous-menus fonctionnent :

Si je clique sur « Gallerie » :

J'ai la bonne page qui s'affiche et l'item « Gallerie » devient actif.

L'item « Les activités » est bien désactivé comme demandé, et j'ai bien le titre et le séparateur :

Finalement la structure du système est simple et fonctionnelle. Le fait d'avoir le menu en JSON permet de le créer de façon visuelle par exemple avec une interface en JavaScript.

XXXVI - Chapitre 36 : Le CMS October

On l'attendait tous et il est enfin arrivé, le premier CMS ambitieux basé sur Laravel 4. Il s'appelle October, il est en version bêta, et nous apporte un peu de fraîcheur automnale alors que l'été approche. Je me suis un peu penché dessus et je vous livre le résultat de mon exploration.

XXXVI-A - Installation d'October

Ça c'est tout simple, il suffit en premier d'aller sur le site :

La mise en page du site cède à la mode Bootstrap mais elle est claire, lisible et agréable. On a le choix pour aller chercher le code :

J'ai opté pour l'installer. J'ai copié le paquet dans un dossier october sur mon site local. On a des fichiers d'installation :

Il faut lancer install.php, on aboutit sur cette page :

L'installation vérifie que toutes les fonctionnalités sont satisfaisantes pour October. Dans mon cas il n'y a pas de souci, alors je poursuis en acceptant les conditions de la licence (MIT).

L'étape suivante consiste à configurer la base de données qui doit déjà exister (et oui l'installateur ne va pas jusqu'à la créer ) :

Après je me suis fait un peu engueuler parce que je n'avais pas précisé de mot de passe pour l'administrateur, mais on me l'avait pas demandé . Bon j'ai complété :

Ensuite on me demande si je veux ajouter des plugins :

Pour le moment je n'en ajoute pas pour avoir un October de base. Je passe donc cette étape et je clique sur le joli bouton « Install ! ». Puis je laisse faire :

Il suffit de quelques minutes pour que tout soit terminé. J'arrive sur cette page :

Apparemment tout s'est bien déroulé. On me demande de supprimer les fichiers qui ont servi à l'installation (bon, ils auraient pu aussi automatiser cette tâche ). Mon dossier est un peu plus garni maintenant :

L'architecture de base de Laravel a été un peu chamboulée. On se rend compte que tout devient accessible, mais c'est sans doute le prix à payer pour l'automatisation. Il faut faire confiance au .htaccess en place. On dispose encore d'Artisan, mais quand on le lance on trouve aussi quelques changements :

1. Laravel Framework version 4.1.29

3. Usage:

4. [options] command [arguments]

6. Options:

7. --help           -h Display this help message.

8. --quiet          -q Do not output any message.

9. --verbose        -v|vv|vvv Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

10. --version        -V Display this application version.

11. --ansi              Force ANSI output.

12. --no-ansi           Disable ANSI output.

13. --no-interaction -n Do not ask any interactive question.

14. --env               The environment the command should run under.

16. Available commands:

17. changes              Display the framework change list

18. clear-compiled       Remove the compiled class file

19. down                 Put the application into maintenance mode

20. dump-autoload        Regenerate framework autoload files

21. env                  Display the current framework environment

22. help                 Displays help for a command

23. list                 Lists commands

24. migrate              Run the database migrations

25. optimize             Optimize the framework for better performance

26. routes               List all registered routes

27. serve                Serve the application on the PHP development server

28. tail                 Tail a log file on a remote server

29. tinker               Interact with your application

30. up                   Bring the application out of maintenance mode

31. asset

32. asset:publish        Publish a package's assets to the public directory

33. cache

34. cache:clear          Flush the application cache

35. command

36. command:make         Create a new Artisan command

37. config

38. config:publish       Publish a package's configuration to the application

39. controller

40. controller:make      Create a new resourceful controller

41. create

42. create:component     Creates a new plugin component.

43. create:controller    Creates a new controller.

44. create:model         Creates a new model.

45. create:plugin        Creates a new plugin.

46. db

47. db:seed              Seed the database with records

48. key

49. key:generate         Set the application key

50. migrate

51. migrate:install      Create the migration repository

52. migrate:make         Create a new migration file

53. migrate:publish      Publish a package's migrations to the application

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

55. migrate:reset        Rollback all database migrations

56. migrate:rollback     Rollback the last database migration

57. october

58. october:down         Destroys all database tables for October and all plugins.

59. october:up           Builds database tables for October and all plugins.

60. october:update       Updates October CMS and all plugins, database and files.

61. plugin

62. plugin:install       Adds a new plugin.

63. plugin:refresh       Removes and re-adds an existing plugin.

64. plugin:remove        Removes an existing plugin.

65. queue

66. queue:failed         List all of the failed queue jobs

67. queue:failed-table   Create a migration for the failed queue jobs database table

68. queue:flush          Flush all of the failed queue jobs

69. queue:forget         Delete a failed queue job

70. queue:listen         Listen to a given queue

71. queue:retry          Retry a failed queue job

72. queue:subscribe      Subscribe a URL to an Iron.io push queue

73. queue:work           Process the next job on a queue

74. session

75. session:table        Create a migration for the session database table

76. view

77. view:publish         Publish a package's views to the application

Le framework est organisé en modules :

On en trouve trois dans l'installation de base. On voit aussi la possibilité d'ajouter des plugins, on en a un de démonstration :

On trouve également un dossier pour les thèmes avec un thème de démonstration :

Un autre dossier est prêt pour les uploads (téléversements) :

XXXVI-B - Le frontend

L'URL de base mène sur une page de démonstration :

Là on nous explique dans un anglais impeccable comment est organisé le front-end (interface utilisateur).

XXXVI-B-1 - Le layout

Un layout est une trame de base pour les pages. Dans la démo on n'en a qu'un :

On peut explorer le code :

1. description = "Default layout"

2. ==

3. <!DOCTYPE html>

4. <html>

5. <head>

6. <title>October CMS - {{ this.page.title }}</title>

7. <meta name="author" content="October CMS">

8. <meta name="viewport" content="width=device-width, initial-scale=1.0">

9. <link rel="icon" type="image/png" href="{{ 'assets/images/october.png'|theme }}" />

10. {% styles %}

11. <link href="{{ [

12. 'assets/css/theme.css'

13. ]|theme }}" rel="stylesheet">

14. </head>

15. <body>

17. <!-- Header -->

18. <header id="layout-header">

20. <!-- Nav -->

21. <nav id="layout-nav" class="navbar navbar-inverse navbar-fixed-top" role="navigation">

22. <div class="container">

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

24. <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-main-collapse">

25. <span class="sr-only">Toggle navigation</span>

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

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

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

29. </button>

30. <a class="navbar-brand" href="{{ 'home'|page }}">October Demo</a>

31. </div>

32. <div class="collapse navbar-collapse navbar-main-collapse">

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

34. <li class="{% if this.page.id == 'home' %}active{% endif %}"><a href="{{ 'home'|page }}">Basic concepts</a></li>

35. <li class="{% if this.page.id == 'ajax' %}active{% endif %}"><a href="{{ 'ajax'|page }}">AJAX framework</a></li>

36. <li class="{% if this.page.id == 'plugins' %}active{% endif %}"><a href="{{ 'plugins'|page }}">Plugin components</a></li>

37. </ul>

38. </div>

39. </div>

40. </nav>

42. </header>

44. <!-- Content -->

45. <section id="layout-content">

46. {% page %}

47. </section>

49. <!-- Footer -->

50. <footer id="layout-footer">

51. {% partial "footer" %}

52. </footer>

54. <!-- Scripts -->

55. <script src="{{ [

56. 'assets/javascript/jquery.js',

57. 'assets/javascript/app.js'

58. ]|theme }}"></script>

59. {% framework extras %}

60. {% scripts %}

62. </body>

63. </html>

October utilise et étend Twig. La variable this contient un objet qui permet par exemple de récupérer l'objet page et ainsi de connaître le titre de cette page :

<title>October CMS - {{ this.page.title }}</title>

L'accès aux assets s'effectue ainsi :

<link rel="icon" type="image/png" href="{{ 'assets/images/october.png'|theme }}" />

Les assets sont dans le dossier assets du thème, par exemple ici cette image :

Le filtre theme permet de générer correctement l'adresse en référant le thème en cours, par exemple dans mon cas j'obtiens :

<link rel="icon" type="image/png" href="/october/themes/demo/assets/images/october.png" />

On trouve la même chose pour le code CSS :

<link href="{{ ['assets/css/theme.css']|theme }}" rel="stylesheet">

Voilà ce que j'obtiens dans ma page :

<link href="/october/combine/03ccf956ac57d4075b7d6ef3e803b95d-1400759953" rel="stylesheet">

On trouve le fichier original ici :

Avec cet avertissement :

1. /*

2. *

3. * !! WARNING !! THIS CSS FILE HAS BEEN AUTOMATICALLY GENERATED !!

4. *

5. * Any modifications made to this file will be lost next time it is generated.

6. * Consider modifying the 'theme.less' file found in the 'less' folder instead

7. * or create a new custom CSS file to override the rules in this one.

8. *

9. * !! YOU HAVE BEEN WARNED !! MODIFY THIS FILE AT YOUR OWN RISK !!

10. *

11. */

Le fichier a été généré à partir de fichiers LESS, on les trouve dans le dossier less :

En creusant un peu on trouve Bootstrap et font-awesome dans le dossier vendor du thème de démo :

On n'est évidemment pas obligé de suivre cette organisation pour nos pages et on peut se contenter de mettre notre CSS dans le dossier.

XXXVI-B-2 - Les pages

Un site Web est fait de pages. On retrouve ce concept dans October avec les pages. Il y a un dossier dans le thème prévu pour elles :

Pour insérer le contenu d'une page dans le layout il est prévu cette syntaxe :

{% page %}

C'est donc ici que la page va s'insérer dans le layout.

De quoi est constituée une page ? De code HTML évidemment mais aussi d'éléments de configuration. Si on regarde la page home.htm on trouve ceci :

1. title = "Demonstration"

2. url = "/"

3. layout = "default"

4. ==

On trouve donc le titre de la page, son URL (ici l'URL de base du site), et le nom du layout. Il est assez inattendu de trouver la spécification de l'URL d'une vue dans son code, mais pourquoi pas ?

Si on regarde le menu dans le layout voici la syntaxe pour générer les liens des pages :

<li class="{% if this.page.id == 'home' %}active{% endif %}"><a href="{{ 'home'|page }}">

On utilise le filtre |page et on met le nom de la page, ici home. Dans mon cas ce code est généré :

<li class="active"><a href="http://localhost/october">Basic concepts</a></li>

XXXVI-B-3 - Les partials

Les partials sont des morceaux de HTML qu'on peut insérer dans des pages. On les trouve dans le dossier partials :

Un bon exemple est le footer :

1. <div id="footer">

2. <div class="container">

3. <p class="muted credit">© 2013 Alexey Bobkov &amp; Samuel Georges.</p>

4. </div>

5. </div>

On l'insère dans le layout avec cette syntaxe :

1. <!-- Footer -->

2. <footer id="layout-footer">

3. {% partial "footer" %}

4. </footer>

XXXVI-B-4 - Les contents blocks

Les contents blocks, c'est un peu comme les partials puisque c'est un morceau de page qui peut être inséré n'importe où, mais c'est leur nature qui est différente. Un partial c'est juste du HTML alors qu'un content block peut aussi être du texte ou du code Markdown qui peut être modifié de façon indépendante des pages et des layouts. On les trouve dans le dossier content :

Pour insérer un bloc dans une page c'est facile, voilà par exemple le bloc welcome.html inséré dans la page home.htm :

{% content "welcome.htm" %}

XXXVI-B-5 - AJAX

October inclut un framework pour AJAX qu'on peut utiliser dans les layouts et les pages. Ce framework peut modifier une page avec des partials. Pour l'utiliser il faut le déclarer, par exemple dans le layout :

1. <script src="{{ [

2. 'assets/javascript/jquery.js',

3. 'assets/javascript/app.js'

4. ]|theme }}"></script>

5. {% framework extras %}

Le framework utilise jQuery, il est donc appelé juste avant.

Il y a une page de démonstration pour ce framework :

C'est une calculette simplifiée. Voilà la déclaration du formulaire :

<form class="form-inline" data-request="onTest" data-request-update="calcresult: '#result'">

Le traitement est assuré par la fonction PHP onTest qui a été prévue dans le même fichier. Au retour on envoie le partial calcresult dans l'id #result.

Le framework a l'air d'être assez sophistiqué, en tout cas la documentation est fournie.

XXXVI-C - Le back-end

Voyons maintenant le back-end (traitement d'arrière-plan) qu'on atteint avec l'URL backend. On tombe évidemment sur une page d'authentification :

Et là, oh surprise ! on me propose déjà une mise à jour :

Une bonne occasion de voir comment ça se passe… Il faut aller dans le système et les mises à jour :

Quand je clique sur le bouton j'obtiens une fenêtre :

Et la mise à jour se télécharge :

Et je passe à la version 90 :

Bon, je ne sais pas du tout ce qui a été modifié, par contre.

On trouve aussi la gestion des administrateurs :

On peut créer d'autres groupes d'administrateurs et spécifier leurs autorisations. Par contre il ne semble pas y avoir de système d'autorisation pour les utilisateurs.

On peut aussi configurer les paramètres pour l'envoi d'emails :

Dans la partie CMS on a accès aux différents constituants et on peut les modifier, en supprimer, ou en ajouter :

On peut créer des pages, y insérer des partials, des composants, changer l'URL ou la description…

L'éditeur de code est évidemment très sommaire mais convient parfaitement pour des petites modifications. Il y a deux onglets : Markup et Code. Quand du PHP est inséré dans une page, on y accède avec le deuxième onglet.

XXXVI-D - Les plugins

Les plugins permettent d'ajouter des fonctionnalités à October. Ils sont donc indispensables pour créer une application. Ils résident dans le dossier plugins :

On voit qu'il y a un plugin de démonstration nommé Todo List. On peut y accéder par le back-end :

Un clic sur le composant ouvre l'inspecteur qui permet de modifier les paramètres :

Et on peut l'expérimenter en front-end :

Il se contente de créer une liste de tâches qu'on entre au clavier. L'insertion dans la page est toute simple :

{% component 'demoTodo' %}

Tout le code se trouve dans le dossier des plugins. La documentation explique en détails comment créer un composant. Mais on peut aussi ajouter un plugin existant en allant voir dans le market :

Bon alors je me lance. Comme j'ai déploré le fait de ne pas disposer d'autorisation en front-end je sélectionne un plugin qui propose ça :

Mais je dois commencer par me créer un compte sur le site :

À ce stade on me demande aussi de créer un projet pour simplifier l'installation et la mise à jour des plugins. J'en crée donc un :

Et j'ajoute le plugin User à ce projet :

Il faut ensuite aller récupérer l'ID du projet dans le compte et attacher ce projet à l'installation :

Ensuite il faut attendre que le plugin se charge et s'installe. Je me retrouve alors avec une nouvelle fonctionnalité dans le back-end :

Je peux créer des utilisateurs, le plugin s'est correctement installé ! Je le trouve dans le dossier des plugins :

Le plugin a d'ailleurs l'air d'être plutôt bien fait. Il y a un composant account :

On va voir si ça fonctionne, je crée une page themes/demo/pages/account.htm :

1. title = "Account"

2. url = "/account/:code?"

3. layout = "default"

5. [account]

6. redirect = "home"

7. paramCode = "code"

8. ==

9. <div class="container">

10. {% component 'account' %}

11. </div>

Notez qu'on peut créer cette page directement à partir du back-end.

J'ajoute aussi un bouton dans mon layout :

1. <div class="collapse navbar-collapse navbar-main-collapse">

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

3. ...

4. <li class="{% if this.page.id == 'plugins' %}active{% endif %}"><a href="{{ 'account'|page }}">Compte</a></li>

5. </ul>

6. </div>

Comme ça j'ai un accès dans le menu :

Et voilà la page résultante :

Il y a aussi une page prévue pour l'oubli du mot de passe et dans le back-end on peut régler pas mal de choses. Par contre il n'a pas été prévu la localisation, donc pour passer en français c'est pas évident . Mais je voulais juste voir comment on installe et utilise un plugin.

XXXVI-E - Conclusion

Il y aurait encore beaucoup à dire sur ce CMS, par exemple concernant les bases de données ou les outils pour créer des pages en back-end. Tout a été bien pensé et élégamment créé. La seule question que je me pose concerne le public visé par ce CMS. Il me semble se trouver un peu à la frontière entre le spécialiste de Laravel qui pourrait préférer utiliser directement le framework sans prendre la peine de devoir passer par un apprentissage supplémentaire même si le CMS semble assez accessible, et le néophyte qui risque d'être un peu perdu lorsqu'il devra nécessairement mettre les doigts dans le cambouis.

Ce CMS mérite d'être connu et expérimenté. On va voir dans les mois à venir si la bibliothèque de plugins s'enrichit et s'il rencontre un engouement du côté des utilisateurs.

XXXVII - Remerciements

Nous remercions Maurice Chavelli qui nous a autorisés à publier ce cours.

Nous tenons également à remercier Winjerome pour la mise au gabarit et Maxy35 pour la relecture orthographique.