Skip to content

3. Le modèle d'une action

Une action a besoin des informations envoyées par le navigateur : les paramètres de l'URL ([?nom=martin&age=42]), les champs d'un formulaire, le corps JSON d'une requête, parfois un entête HTTP. Le cours d'origine appelle modèle de l'action l'ensemble de ces informations. Ce chapitre montre comment une action [Laravel] les obtient, les convertit dans le bon type et les valide. Ce travail s'appelle la liaison de données (*data binding*).

[Laravel] donne accès aux informations de la requête par l'objet [Request] (exemple 05), sous forme de chaînes de caractères, avec quelques méthodes de conversion. Pour vérifier les données, il possède un système de validation complet : on décrit les données attendues par des règles (« obligatoire », « un entier entre 0 et 150 »...), et [Laravel] vérifie que les données les respectent. Nous lirons d'abord les paramètres à la main (exemple 07), puis nous confierons leur vérification à ces règles (exemples 08 à 12). Au chapitre suivant, le même système validera les formulaires HTML.

Attention au vocabulaire : le mot « modèle » désignera surtout, au chapitre suivant, les données qu'une action transmet à sa vue. Le modèle d'une action, au sens de ce chapitre, est ce qu'elle reçoit. [Laravel] emploie aussi le mot *model* pour les classes qui représentent les tables d'une base de données (chapitre 9) : c'est un troisième sens.

Les exemples 07 à 12 se lancent comme ceux du chapitre précédent : un terminal ouvert sur le dossier de l'exemple, [php artisan serve] ; [Ctrl-C] arrête le serveur. Leurs fichiers [bootstrap/app.php] se ressemblent tous : ils ajoutent le middleware [JsonLisible] de l'exemple 03, pour un JSON lisible. Nous ne les commenterons que lorsqu'ils apportent quelque chose de nouveau.

3.1. Exemple 07 : les paramètres de l'URL

Les paramètres de l'URL sont la partie qui suit le [?] : [?nom=martin&age=42]. Cette partie s'appelle la chaîne de requête (*query string*). Chaque paramètre est un couple [nom=valeur] ; les couples sont séparés par des [&]. Le contrôleur [ParametresController] :

<?php
// app/Http/Controllers/ParametresController.php : les paramètres de l'URL (la "query string")

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ParametresController
{
    // /parametres/action01?nom=martin : query rend la valeur du paramètre, ou
    // null s'il est absent
    public function action01(Request $request): string
    {
        $nom = $request->query('nom');
        return "nom = [$nom], de type ".get_debug_type($nom);
    }

    // /parametres/action02?age=42 : integer CONVERTIT la valeur (une chaîne) en
    // entier ; un paramètre absent, ou qui n'est pas un entier, donne 0
    public function action02(Request $request): string
    {
        $age = $request->integer('age');
        return "age = [$age], de type ".get_debug_type($age);
    }

    // /parametres/action03?age=42 : un paramètre OBLIGATOIRE ; son absence, ou une
    // valeur qui n'est pas un entier, arrête l'action par une réponse 400 (Bad Request)
    public function action03(Request $request): string
    {
        $texte = $request->query('age');
        if ($texte === null) {
            // abort lève une exception, que Laravel transforme en réponse d'erreur
            abort(400, 'le paramètre [age] est obligatoire');
        }
        // la conversion est à notre charge : filter_var rend l'entier, ou false
        $age = filter_var($texte, FILTER_VALIDATE_INT);
        if ($age === false) {
            abort(400, "[$texte] n'est pas un nombre entier");
        }
        return "age = [$age], de type ".get_debug_type($age);
    }

    // /parametres/action04?page=2 : une valeur par défaut si le paramètre manque
    public function action04(Request $request): string
    {
        $page = $request->integer('page', 1);
        return "page = [$page]";
    }

    // /parametres/action05?q=laravel : le nom du paramètre de l'URL n'est pas forcément
    // celui de la variable PHP
    public function action05(Request $request): string
    {
        $recherche = $request->query('q');
        return "recherche = [$recherche]";
    }

    // /parametres/action06?nom=martin&age=42 : tous les paramètres d'un coup, en tableau
    public function action06(Request $request): array
    {
        return $request->query();
    }
}
  • ligne 14 : la méthode [query] de la requête rend la valeur d'un paramètre de l'URL, déjà décodée, ou [null] s'il est absent : elle ne provoque jamais d'erreur ;
  • ligne 15 : l'action affiche la valeur, et son type : la fonction [get_debug_type] rend le nom du type d'une valeur. Une valeur tirée de l'URL est toujours une chaîne ([string]). L'opérateur [.] concatène la chaîne interpolée et le type ;
  • ligne 22 : la méthode [integer] lit le paramètre et le convertit en entier. Si la conversion échoue ([abc]), ou si le paramètre est absent, elle rend 0 : l'action ne peut pas distinguer ces cas d'un âge de 0 saisi pour de bon, et l'erreur de saisie passe inaperçue. Il existe de même [float], [boolean] ([1], [true], [on], [yes] sont vrais), [string], [date] (exemple 12), [enum] ;
  • lignes 30-40 : un paramètre obligatoire, et une conversion faite à la main, qui distingue les deux erreurs possibles. [abort(400, message)] arrête l'action par une réponse 400 (*Bad Request*, la requête est incorrecte), accompagnée du message (cf. exemple 03). La fonction [filter_var] de [PHP], avec le filtre [FILTER_VALIDATE_INT], rend l'entier contenu dans la chaîne, ou [false] si la chaîne n'est pas un entier ;
  • ligne 46 : le second paramètre de [integer] est une valeur par défaut, utilisée si le paramètre manque. C'est le cas typique du numéro de page d'une liste ;
  • ligne 54 : rien n'oblige à nommer la variable [PHP] comme le paramètre de l'URL. Les moteurs de recherche utilisent souvent un paramètre court, [q] (*query*) ;
  • ligne 61 : sans paramètre, [query] rend tous les paramètres de l'URL, dans un tableau associatif, que l'action rend : [Laravel] le transforme en JSON (cf. exemple 03).

La table de routage déclare les six routes par une boucle :

<?php
// routes/web.php : les routes de l'exemple 07

use App\Http\Controllers\ParametresController;
use Illuminate\Support\Facades\Route;

Route::prefix('parametres')->controller(ParametresController::class)->group(function () {
    foreach (['action01', 'action02', 'action03', 'action04', 'action05', 'action06'] as $action) {
        Route::get($action, $action);
    }
});
  • ligne 8 : la boucle [foreach] parcourt un tableau : à chaque tour, la variable [$action] prend la valeur de l'élément suivant. Chaque route porte le nom de son action : [Route::get('action01', 'action01')]... La table de routage est un fichier [PHP] comme un autre : on peut y écrire tout le code que l'on veut.

Testons chaque action avec [curl]. Un paramètre présent, puis absent :

curl -i "http://localhost:8000/parametres/action01?nom=martin"
curl http://localhost:8000/parametres/action01
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

nom = [martin], de type string
nom = [], de type null

Les guillemets autour de l'URL sont nécessaires dans un terminal : sans eux, le shell interpréterait le [&] (qui lance une commande en arrière-plan sous [Linux]) et le [?]. La valeur [null], insérée dans une chaîne, devient une chaîne vide. Une valeur contenant des espaces ou des caractères spéciaux doit être encodée dans l'URL : l'espace devient [%20], le [&] devient [%26] (sinon, il séparerait deux paramètres). Le navigateur le fait tout seul ; avec [curl], il faut l'écrire. [PHP] décode la valeur avant que l'action ne la lise :

curl "http://localhost:8000/parametres/action01?nom=jean%20dupont%20%26%20fils"
nom = [jean dupont & fils], de type string

La conversion par [integer], réussie, ratée, puis sans paramètre :

1
2
3
curl "http://localhost:8000/parametres/action02?age=42"
curl "http://localhost:8000/parametres/action02?age=abc"
curl http://localhost:8000/parametres/action02
1
2
3
age = [42], de type int
age = [0], de type int
age = [0], de type int

Le paramètre obligatoire de l'action 03, absent puis invalide. Nous demandons une réponse JSON (entête [Accept], cf. exemple 03), qui contient le message de [abort] :

curl -i -H "Accept: application/json" http://localhost:8000/parametres/action03
curl -H "Accept: application/json" "http://localhost:8000/parametres/action03?age=abc"
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
    "message": "le paramètre [age] est obligatoire",
    "exception": "Symfony\\Component\\HttpKernel\\Exception\\HttpException",
    ...
}
{
    "message": "[abc] n'est pas un nombre entier",
    "exception": "Symfony\\Component\\HttpKernel\\Exception\\HttpException",
    ...
}

Dans un navigateur, en mode debug, une réponse 400 affiche la page d'erreur de [Laravel], avec le message :

Image

La valeur par défaut, et le paramètre renommé :

1
2
3
curl http://localhost:8000/parametres/action04
curl "http://localhost:8000/parametres/action04?page=3"
curl "http://localhost:8000/parametres/action05?q=laravel"
1
2
3
page = [1]
page = [3]
recherche = [laravel]

Enfin, tous les paramètres d'un coup. Le paramètre [nom] apparaît deux fois :

curl "http://localhost:8000/parametres/action06?nom=martin&age=42&nom=durand"
1
2
3
4
{
    "nom": "durand",
    "age": "42"
}

Le paramètre [nom] ne garde que sa dernière valeur : c'est une règle de [PHP], que nous retrouverons à l'exemple 10. L'âge est resté une chaîne (["42"], entre guillemets dans le JSON) : aucune conversion n'a été demandée. Dans un navigateur (ici [Chromium]), le JSON est présenté tel que le serveur l'a mis en forme ; la case « Pretty-print » le présenterait indenté s'il ne l'était pas. La barre d'adresse montre au passage l'encodage des caractères accentués dans l'URL ([é] devient [%C3%A9], ses deux octets en [UTF-8]) :

Image

Ce premier exemple montre les limites de l'approche manuelle : pour chaque paramètre, il faut écrire la lecture, la conversion, le traitement de l'absence et de l'erreur. Avec dix paramètres, le code de l'action serait noyé sous ces vérifications. Il vaut mieux décrire ce qu'on attend, et laisser le framework le vérifier. C'est l'objet de l'exemple suivant.

3.2. Exemple 08 : un modèle d'action décrit par une classe

[Laravel] vérifie les données d'une requête grâce à des règles de validation. Une règle a un nom ([required], [integer], [email]...), parfois des paramètres ([min:2], [between:0,150]) ; les règles d'un champ s'écrivent dans une chaîne, séparées par des [|], ou dans un tableau. [Laravel] en fournit une centaine. L'endroit naturel pour décrire le modèle d'une action est une requête de formulaire (*form request*) : une classe qui hérite de [FormRequest], et dont la méthode [rules] rend les règles de chaque paramètre.

3.2.1. Le modèle : app/Http/Requests/PersonneRequest.php

<?php
// app/Http/Requests/PersonneRequest.php : le modèle de l'action, une classe qui
// décrit les paramètres attendus et leurs contraintes

namespace App\Http\Requests;

use App\Models\Personne;
use Illuminate\Foundation\Http\FormRequest;

class PersonneRequest extends FormRequest
{
    // les RÈGLES de validation, paramètre par paramètre
    public function rules(): array
    {
        return [
            // obligatoire, une chaîne de 2 à 20 caractères
            'nom' => 'required|string|min:2|max:20',
            // obligatoire, un entier compris entre 0 et 150
            'age' => 'required|integer|between:0,150',
            // l'une de ces trois valeurs seulement
            'titre' => 'required|in:Mr,Mme,Melle',
            // facultatif (nullable) ; s'il est présent, une adresse électronique valide
            'email' => 'nullable|email',
        ];
    }

    // les paramètres validés, sous la forme d'un objet typé
    public function personne(): Personne
    {
        $valeurs = $this->validated();
        return new Personne($valeurs['nom'], (int) $valeurs['age'], $valeurs['titre'],
            $valeurs['email'] ?? null);
    }
}
  • ligne 10 : la classe hérite ([extends]) de [FormRequest], qui hérite elle-même de [Request] : un objet [PersonneRequest] est une requête, avec toutes les méthodes de l'exemple 07, et, en plus, des règles de validation ;
  • lignes 13-24 : les règles, dans un tableau associatif : pour chaque paramètre, la chaîne de ses règles ;
  • ligne 17 : [required] : le paramètre est obligatoire (absent, ou vide, il est refusé) ; [string] : une chaîne ; [min:2] et [max:20] : pour une chaîne, sa longueur doit être comprise entre 2 et 20 caractères ;
  • ligne 19 : [integer] : la valeur doit représenter un entier (["42"] convient, ["abc"] non) ; [between:0,150] : pour un nombre, sa valeur doit être comprise entre 0 et 150. Les règles [min], [max] et [between] s'adaptent ainsi au type de la valeur : une longueur pour une chaîne, une valeur pour un nombre, un nombre d'éléments pour un tableau (exemple 10) ;
  • ligne 21 : [in:Mr,Mme,Melle] : l'une des trois valeurs citées, et elles seules ;
  • ligne 23 : [nullable] : le paramètre est facultatif (absent ou vide, il est accepté) ; [email] : s'il est présent, une adresse électronique valide ;
  • lignes 28-33 : une méthode ajoutée à la classe : elle rend les données validées sous la forme d'un objet [Personne] (ci-dessous). [$this->validated()] rend les données qui ont été validées, et elles seules : un paramètre sans règle n'y figure pas. La validation de [Laravel] vérifie les données, mais ne les convertit pas : l'âge validé est toujours la chaîne ["42"]. C'est l'opérateur de conversion [(int)] qui en fait un entier. L'opérateur [??] rend son opérande de gauche s'il existe et n'est pas [null], celui de droite sinon : [$valeurs['email'] ?? null] vaut [null] si l'adresse est absente du tableau.

Ce modèle décrit les données attendues ; il ne contient aucune instruction de vérification. C'est ce qu'on appelle un style déclaratif. La classe [Personne] est une classe immuable, sur le modèle de celle de l'exemple 03 :

<?php
// app/Models/Personne.php : une personne

namespace App\Models;

readonly class Personne
{
    public function __construct(
        public string $nom,
        public int $age,
        public string $titre,
        public ?string $email,
    ) {
    }
}
  • ligne 12 : [?string] : une chaîne, ou [null].

3.2.2. L'action : app/Http/Controllers/PersonnesController.php

<?php
// app/Http/Controllers/PersonnesController.php : une action dont le modèle est une classe FormRequest

namespace App\Http\Controllers;

use App\Http\Requests\PersonneRequest;

class PersonnesController
{
    // /personnes/action01?nom=martin&age=42&titre=Mr
    // Le paramètre de type PersonneRequest est VALIDÉ par Laravel AVANT l'appel de
    // l'action : un modèle invalide lève une ValidationException, transformée en
    // réponse 400 par bootstrap/app.php, et l'action n'est jamais appelée
    public function action01(PersonneRequest $request): array
    {
        $personne = $request->personne();
        return [
            'recu' => $request->validated(),
            'personne' => $personne,
            'typeAge' => get_debug_type($personne->age),
        ];
    }
}
  • ligne 14 : l'action demande un paramètre de type [PersonneRequest], là où l'exemple 07 demandait un [Request]. [Laravel] fabrique alors un objet [PersonneRequest] à partir de la requête en cours, et le valide avant d'appeler l'action : il vérifie chaque règle de chaque paramètre. Si toutes sont respectées, l'action est appelée. Sinon, [Laravel] lève une exception [ValidationException], qui contient la liste de toutes les erreurs (et pas seulement la première), et l'action n'est jamais appelée : elle n'a donc pas à se soucier des données invalides ;
  • lignes 16-20 : l'action rend les données validées, telles quelles, puis sous forme d'objet [Personne], et le type de l'âge, pour montrer la différence.

Qui traite l'exception [ValidationException] ? Par défaut, [Laravel] lui-même, et sa réponse dépend du client. Pour une requête qui attend du JSON (entête [Accept: application/json]), c'est une réponse 422 (*Unprocessable Content*), qui décrit les erreurs en JSON. Pour une requête ordinaire, venue d'un formulaire HTML, c'est une redirection vers la page précédente, les erreurs étant conservées pour le formulaire qui sera réaffiché (nous le verrons au chapitre suivant). Ici, nous voulons une réponse d'erreur en JSON, dans tous les cas, avec le code 400. C'est le rôle de [bootstrap/app.php].

3.2.3. Le traitement de l'erreur : bootstrap/app.php

<?php
// bootstrap/app.php : l'application de l'exemple 08

use App\Http\Middleware\JsonLisible;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Validation\ValidationException;

(require __DIR__.'/../../vendor/autoload.php')->addPsr4('App\\', __DIR__.'/../app/');

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(web: __DIR__.'/../routes/web.php')
    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->append(JsonLisible::class);
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        // un modèle invalide : l'exception ValidationException devient, pour
        // TOUTE l'application, une réponse 400 qui détaille les erreurs
        $exceptions->render(function (ValidationException $e) {
            $erreurs = [];
            // errors() : champ -> liste de ses messages d'erreur
            foreach ($e->errors() as $champ => $messages) {
                foreach ($messages as $message) {
                    $erreurs[] = ['champ' => $champ, 'message' => $message];
                }
            }
            return response()->json(['statut' => 400, 'erreurs' => $erreurs], 400);
        });
    })
    ->create();
  • lignes 17-30 : la méthode [withExceptions] reçoit cette fois une fonction, qui reçoit l'objet [$exceptions] décrivant le traitement des exceptions de l'application ;
  • lignes 20-29 : la méthode [render] enregistre un gestionnaire d'exceptions : une fonction que [Laravel] appelle quand une action lève une exception du type de son paramètre (ici [ValidationException], ou une classe dérivée), et dont la valeur de retour est la réponse envoyée. [Laravel] reconnaît le type de l'exception traitée au type du paramètre de la fonction. Déclaré dans [bootstrap/app.php], le gestionnaire vaut pour toutes les actions de l'application : chaque action qui valide un modèle en profite, sans écrire une ligne de traitement d'erreur ;
  • lignes 23-27 : [$e->errors()] rend les erreurs, dans un tableau associatif : pour chaque champ en erreur, la liste de ses messages. La boucle [foreach ($tableau as $cle => $valeur)] parcourt à la fois les clés et les valeurs. [$erreurs[] = ...] ajoute un élément à la fin du tableau [$erreurs] ;
  • ligne 28 : une réponse JSON, avec le code 400.

3.2.4. Exécution

Un modèle valide :

curl -i "http://localhost:8000/personnes/action01?nom=martin&age=42&titre=Mr"
HTTP/1.1 200 OK
Content-Type: application/json

{
    "recu": {
        "nom": "martin",
        "age": "42",
        "titre": "Mr"
    },
    "personne": {
        "nom": "martin",
        "age": 42,
        "titre": "Mr",
        "email": null
    },
    "typeAge": "int"
}

Les données validées ne contiennent que les paramètres reçus, l'âge sous forme de chaîne ; l'objet [Personne] contient l'âge sous forme d'entier (sans guillemets dans le JSON), et l'adresse absente vaut [null]. Un nom entouré d'espaces ([%20]) et une adresse présente :

curl "http://localhost:8000/personnes/action01?nom=%20%20martin%20&age=42&titre=Mme&email=martin@exemple.fr"
{
    "recu": {
        "nom": "martin",
        "age": "42",
        "titre": "Mme",
        "email": "martin@exemple.fr"
    },
    "personne": {
        "nom": "martin",
        "age": 42,
        "titre": "Mme",
        "email": "martin@exemple.fr"
    },
    "typeAge": "int"
}

Les espaces qui entouraient le nom ont disparu, sans que nous l'ayons demandé : deux middlewares, exécutés par [Laravel] pour toutes les requêtes, transforment les paramètres reçus avant qu'une action ne les lise. [TrimStrings] retire les espaces qui entourent chaque chaîne ; [ConvertEmptyStringsToNull] remplace chaque chaîne vide par [null]. Aucun paramètre maintenant : les trois champs obligatoires manquent.

curl -i http://localhost:8000/personnes/action01
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
    "statut": 400,
    "erreurs": [
        {
            "champ": "nom",
            "message": "The nom field is required."
        },
        {
            "champ": "age",
            "message": "The age field is required."
        },
        {
            "champ": "titre",
            "message": "The titre field is required."
        }
    ]
}

Quatre champs invalides à la fois :

curl "http://localhost:8000/personnes/action01?nom=m&age=abc&titre=Mlle&email=xx"
{
    "statut": 400,
    "erreurs": [
        {
            "champ": "nom",
            "message": "The nom field must be at least 2 characters."
        },
        {
            "champ": "age",
            "message": "The age field must be an integer."
        },
        {
            "champ": "titre",
            "message": "The selected titre is invalid."
        },
        {
            "champ": "email",
            "message": "The email field must be a valid email address."
        }
    ]
}

Toutes les erreurs sont signalées en une fois, ce qui permet à l'utilisateur de tout corriger d'un coup. Pour l'âge [abc], seule la règle [integer] est signalée : la règle [between] n'a pas de sens pour une valeur qui n'est pas un nombre. Les messages sont en anglais, la langue par défaut de [Laravel]. L'exemple suivant les remplace par des messages français.

3.3. Exemple 09 : la validation faite par l'action

Dans l'exemple 08, un modèle invalide interrompt l'action, et c'est le gestionnaire de l'application qui répond. C'est commode pour une API, dont les clients sont des programmes qui attendent une réponse d'erreur normalisée. Mais une page web préfère souvent réafficher le formulaire avec les messages d'erreur : c'est alors l'action qui doit décider de ce qu'elle fait d'un modèle invalide. Les classes [PersonneRequest] et [Personne] sont celles de l'exemple 08.

09-validation-action/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   └── PersonnesController.php
│   │   └── Requests/
│   │       └── PersonneRequest.php
│   └── Models/
│       └── Personne.php
├── lang/
│   └── fr/
│       └── validation.php
└── ...

3.3.1. Les messages en français : lang/fr/validation.php

Les messages d'erreur de [Laravel] ne sont pas écrits dans son code : chaque message est désigné par une clé, et un fichier de langue associe à chaque clé son texte. Les fichiers anglais sont livrés avec le framework ; pour une autre langue, l'application fournit les siens, dans son dossier [lang], un sous-dossier par langue. La langue de l'application est fixée par le fichier [.env] :

APP_LOCALE=fr

Le fichier des messages de validation s'appelle [validation.php] :

<?php
// lang/fr/validation.php : les messages des erreurs de validation, en français.
// Chaque message est désigné par le nom de sa RÈGLE (required, min...). Les marques
// :attribute, :min, :max... sont remplacées par le nom du champ et par les
// paramètres de la règle
return [
    'required' => 'le champ est obligatoire',
    'string' => 'une chaîne de caractères est attendue',
    'integer' => 'un nombre entier est attendu',
    'email' => 'adresse électronique invalide',
    'in' => 'valeur inconnue',
    // certaines règles ont un message par type de valeur : pour une chaîne, min
    // porte sur sa longueur ; pour un nombre, sur sa valeur ; pour un tableau, sur
    // son nombre d'éléments
    'min' => [
        'string' => ':min caractère(s) au moins',
        'numeric' => 'la valeur doit être supérieure ou égale à :min',
        'array' => ':min élément(s) au moins',
    ],
    'max' => [
        'string' => ':max caractère(s) au plus',
        'numeric' => 'la valeur doit être inférieure ou égale à :max',
    ],
    'between' => [
        'numeric' => 'la valeur doit être comprise entre :min et :max',
    ],
    'array' => 'une liste est attendue',
];
  • ligne 6 : un fichier de langue est un fichier [PHP] qui rend un tableau associatif : pour chaque clé, son texte ;
  • lignes 7-11 : la clé d'un message de validation est le nom de la règle qui a échoué. Cette technique (traduire à partir du nom de l'erreur, et non de son texte anglais) est celle que l'on retrouvera dans l'étude de cas : les textes y sont désignés par des clés, traduites dans la langue de l'utilisateur ;
  • lignes 15-19 : les règles qui s'adaptent au type de la valeur (exemple 08) ont un message par type : [string], [numeric], [array]. Les marques [:min], [:max] sont remplacées par les paramètres de la règle ([min:2] donne « 2 caractère(s) au moins »). La marque [:attribute], que nous n'utilisons pas, serait remplacée par le nom du champ.

Une clé absente de ce fichier garde son message anglais : [Laravel] se replie sur la langue de secours, [APP_FALLBACK_LOCALE], l'anglais par défaut. Des traductions complètes des messages de [Laravel], dans de nombreuses langues, sont publiées par la communauté (le paquet [laravel-lang]).

3.3.2. L'action : app/Http/Controllers/PersonnesController.php

<?php
// app/Http/Controllers/PersonnesController.php : c'est l'action qui décide de ce qu'elle fait d'un modèle invalide

namespace App\Http\Controllers;

use App\Http\Requests\PersonneRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class PersonnesController
{
    public function action01(Request $request): string
    {
        // un VALIDATEUR : les données à valider, et les règles (celles de la
        // classe PersonneRequest de l'exemple 08)
        $validateur = Validator::make($request->query(), (new PersonneRequest())->rules());
        if ($validateur->fails()) {
            // les erreurs, champ par champ : errors()->messages() en donne la liste
            $items = '';
            foreach ($validateur->errors()->messages() as $champ => $messages) {
                $items .= "<li>$champ : $messages[0]</li>";
            }
            return "<p>Le formulaire contient des erreurs :</p><ul>$items</ul>";
        }
        // les données validées
        $personne = $validateur->validated();
        // e() : le nom saisi est inséré dans du HTML ; ses caractères < > & " '
        // sont remplacés par &lt; &gt;... (sinon, une saisie pourrait injecter du HTML)
        return "<p>Bienvenue {$personne['titre']} ".e($personne['nom'])
            ." ({$personne['age']} ans)</p>";
    }
}
  • ligne 16 : la façade [Validator] fabrique un validateur : un objet qui applique des règles à des données. Les données sont les paramètres de l'URL ; les règles sont celles de la classe [PersonneRequest] de l'exemple 08. Une requête de formulaire est une classe comme une autre : on peut en créer un objet, par [new], pour lire ses règles ;
  • ligne 17 : [fails()] exécute la validation, et rend [true] si une règle au moins a échoué. L'action décide elle-même de la suite : aucune exception n'est levée ;
  • lignes 19-23 : les erreurs, champ par champ. [errors()] rend un objet [MessageBag] (un « sac » de messages), dont la méthode [messages()] rend le tableau associatif champ → liste de messages. Pour chaque champ, on garde le premier message, [$messages[0]], dans une balise [<li>]. L'opérateur [.=] ajoute une chaîne à la fin d'une autre ;
  • ligne 26 : les données validées, dans un tableau associatif ;
  • lignes 29-30 : en cas de succès, un message de bienvenue. Dans une chaîne entre guillemets, [{$personne['titre']}] insère un élément de tableau (les accolades délimitent l'expression). Le nom est passé par la fonction [e] de [Laravel] (pour *escape*), qui remplace les caractères qui ont un sens en HTML ([<], [>], [&], ["], [']) par leurs entités ([&lt;], [&gt;]...) : ils s'afficheront tels quels au lieu d'être interprétés. Sans cette précaution, un utilisateur pourrait saisir un « nom » contenant du HTML, voire du [JavaScript], qui s'exécuterait dans le navigateur de quiconque afficherait la page. C'est la faille de sécurité la plus répandue du web, l'injection de script (*XSS*, *cross-site scripting*). Le titre et l'âge n'ont pas besoin de cette protection : leurs valeurs ont été vérifiées par les règles (l'une des trois valeurs permises, un entier). Au chapitre suivant, le moteur de vues [Blade] fera cet échappement automatiquement, pour toute valeur insérée dans une page.

Une saisie valide, puis une saisie comportant quatre erreurs :

Image

Image

La réponse brute de la seconde requête, et celle d'un nom qui contient du HTML :

curl "http://localhost:8000/personnes/action01?nom=m&age=abc&titre=Mx&email=xx"
curl "http://localhost:8000/personnes/action01?nom=%3Cb%3Emartin%3C/b%3E&age=42&titre=Mr"
<p>Le formulaire contient des erreurs :</p><ul><li>nom : 2 caractère(s) au moins</li><li>age : un nombre entier est attendu</li><li>titre : valeur inconnue</li><li>email : adresse électronique invalide</li></ul>
<p>Bienvenue Mr &lt;b&gt;martin&lt;/b&gt; (42 ans)</p>

Le nom [<b>martin</b>] (encodé [%3Cb%3E...] dans l'URL) est affiché tel quel, sans être mis en gras : ses balises ont été neutralisées. Remarquons que la réponse est cette fois un code 200 : c'est l'action qui a choisi de rendre une page normale contenant les erreurs.

3.4. Exemple 10 : les paramètres multiples

Un même paramètre peut apparaître plusieurs fois dans une URL : [?couleur=rouge&couleur=vert]. C'est ce qu'envoie un navigateur pour un groupe de cases à cocher de même nom, ou une liste à sélection multiple (chapitre suivant). [PHP] traite ce cas d'une façon qui lui est propre. Le contrôleur [TableauxController] :

<?php
// app/Http/Controllers/TableauxController.php : un paramètre qui apparaît plusieurs fois dans l'URL

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class TableauxController
{
    // /tableaux/action01?couleur=rouge&couleur=vert : pour PHP, le second paramètre
    // REMPLACE le premier : seule la DERNIÈRE valeur est conservée
    public function action01(Request $request): array
    {
        return ['couleur' => $request->query('couleur'), 'brut' => $request->server('QUERY_STRING')];
    }

    // /tableaux/action02?couleur[]=rouge&couleur[]=vert : les crochets demandent
    // à PHP de ranger TOUTES les valeurs dans un tableau
    public function action02(Request $request): array
    {
        // array : le paramètre sous forme de tableau (vide s'il est absent)
        $couleurs = $request->array('couleur');
        return ['couleur' => $couleurs, 'nombre' => count($couleurs)];
    }

    // /tableaux/action03?couleur[]=rouge : un tableau, validé élément par élément
    public function action03(Request $request): array
    {
        $couleurs = $request->array('couleur');
        $validateur = Validator::make(['couleur' => $couleurs], [
            // une liste d'au moins un élément
            'couleur' => 'array|min:1',
            // chaque élément (*) : "rouge", "vert" ou "bleu"
            'couleur.*' => 'in:rouge,vert,bleu',
        ]);
        $erreurs = [];
        foreach ($validateur->errors()->messages() as $champ => $messages) {
            $erreurs[] = "$champ : $messages[0]";
        }
        return ['couleurs' => $couleurs, 'valide' => ! $erreurs, 'erreurs' => $erreurs];
    }
}
  • ligne 15 : pour [PHP], les paramètres de l'URL forment un tableau associatif, dont chaque clé n'a qu'une valeur : un paramètre répété remplace le précédent, et seule la dernière valeur est conservée. L'action rend aussi, pour comparaison, la chaîne de paramètres brute, telle que le navigateur l'a envoyée : [$request->server(...)] lit les variables que le serveur web transmet à [PHP], dont [QUERY_STRING] ;
  • lignes 20-24 : pour recevoir toutes les valeurs, le nom du paramètre doit se terminer par des crochets : [?couleur[]=rouge&couleur[]=vert]. [PHP] range alors les valeurs dans un tableau, la valeur du paramètre [couleur]. La méthode [array] de la requête rend le paramètre sous forme de tableau : vide s'il est absent, à un seul élément s'il n'a qu'une valeur sans crochets ;
  • lignes 31-36 : le tableau est validé par des règles. [array|min:1] : un tableau d'au moins un élément (la règle [min] porte sur le nombre d'éléments d'un tableau, cf. exemple 08). La clé [couleur.*] désigne chaque élément du tableau : chacun doit être l'une des trois couleurs ;
  • ligne 41 : l'opérateur [!] (non) rend [true] pour un tableau vide, qui vaut « faux » dans une condition.

Le fichier [lang/fr/validation.php] est celui de l'exemple 09. Les résultats :

1
2
3
curl "http://localhost:8000/tableaux/action01?couleur=rouge&couleur=vert"
curl -g "http://localhost:8000/tableaux/action02?couleur[]=rouge&couleur[]=vert"
curl http://localhost:8000/tableaux/action02
{
    "couleur": "vert",
    "brut": "couleur=rouge&couleur=vert"
}
{
    "couleur": [
        "rouge",
        "vert"
    ],
    "nombre": 2
}
{
    "couleur": [],
    "nombre": 0
}

L'option [-g] de [curl] l'empêche d'interpréter lui-même les crochets de l'URL. Un navigateur, lui, les encode ([couleur%5B%5D=rouge]), ce que [PHP] comprend aussi. Côté HTML, il suffit de nommer [couleur[]] les cases à cocher d'un groupe : c'est ce que fera l'exemple 18. Cette règle est propre à [PHP] : les frameworks des autres langages ([Flask], [Spring MVC], [ASP.NET Core]) conservent toutes les valeurs d'un paramètre répété, sans crochets.

La validation, réussie, puis ratée pour une couleur inconnue, puis pour une liste vide :

1
2
3
curl -g "http://localhost:8000/tableaux/action03?couleur[]=rouge&couleur[]=bleu"
curl -g "http://localhost:8000/tableaux/action03?couleur[]=rouge&couleur[]=jaune"
curl http://localhost:8000/tableaux/action03
{
    "couleurs": [
        "rouge",
        "bleu"
    ],
    "valide": true,
    "erreurs": []
}
{
    "couleurs": [
        "rouge",
        "jaune"
    ],
    "valide": false,
    "erreurs": [
        "couleur.1 : valeur inconnue"
    ]
}
{
    "couleurs": [],
    "valide": false,
    "erreurs": [
        "couleur : 1 élément(s) au moins"
    ]
}

L'erreur sur la couleur inconnue désigne [couleur.1] : le champ et l'indice de l'élément fautif dans le tableau (le deuxième, les indices commençant à 0).

3.5. Exemple 11 : les paramètres d'une requête POST

Les exemples précédents utilisaient des requêtes GET, dont les paramètres sont dans l'URL. Une requête POST transporte ses données dans son corps, sous deux formes principales :

  • les champs d'un formulaire HTML : le corps a la même syntaxe qu'une chaîne de requête ([nom=martin&age=42]), et son type (entête [Content-Type]) est [application/x-www-form-urlencoded] ;
  • un document JSON, envoyé par un programme ([JavaScript] dans une page, client d'une API...) : le type est [application/json].

Le contrôleur [PostController] :

<?php
// app/Http/Controllers/PostController.php : les paramètres d'une requête POST

namespace App\Http\Controllers;

use App\Http\Requests\PersonneRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use JsonException;

class PostController
{
    // les champs d'un formulaire HTML (corps application/x-www-form-urlencoded) :
    // post() rend les paramètres du CORPS de la requête, sous forme de tableau
    public function action01(Request $request): array
    {
        return [
            'typeRecu' => $request->header('Content-Type'),
            'champs' => $request->post(),
        ];
    }

    // un corps JSON : json() le désérialise ; isJson() teste le type annoncé
    public function action02(Request $request): array
    {
        if (! $request->isJson()) {
            abort(415, 'le corps de la requête doit être du JSON');
        }
        // json_decode, avec JSON_THROW_ON_ERROR : un JSON mal formé lève une exception
        try {
            $corps = json_decode($request->getContent(), true, flags: JSON_THROW_ON_ERROR);
        } catch (JsonException $e) {
            abort(400, 'JSON mal formé : '.$e->getMessage());
        }
        return ['typeRecu' => $request->header('Content-Type'), 'corps' => $corps,
            'nom' => $request->input('nom')];
    }

    // un seul champ du formulaire
    public function action03(Request $request): string
    {
        $nom = $request->input('nom');
        return "nom = [$nom]";
    }

    // les champs du formulaire, validés par les règles de la classe PersonneRequest
    public function action04(Request $request): array
    {
        $validateur = Validator::make($request->post(), (new PersonneRequest())->rules());
        $erreurs = [];
        foreach ($validateur->errors()->messages() as $champ => $messages) {
            $erreurs[] = "$champ : $messages[0]";
        }
        sort($erreurs);
        return ['personne' => $erreurs ? null : $validateur->validated(),
            'valide' => ! $erreurs, 'erreurs' => $erreurs];
    }
}
  • ligne 18 : l'entête [Content-Type] de la requête ;
  • ligne 19 : la méthode [post] rend les paramètres du corps de la requête, dans un tableau associatif ; [post('nom')] rendrait un seul paramètre. Les champs d'un formulaire suivent les règles de l'exemple 10 : un champ répété doit avoir un nom terminé par [[]] ;
  • lignes 26-27 : [isJson()] teste le type annoncé par l'entête [Content-Type]. Un corps qui n'est pas déclaré comme du JSON est refusé par une erreur 415 (*Unsupported Media Type*) ;
  • lignes 30-34 : [getContent()] rend le corps brut de la requête, une chaîne ; la fonction [json_decode] de [PHP] le désérialise : le texte JSON devient une valeur [PHP] (le second paramètre, [true], demande des tableaux associatifs pour les objets JSON). Avec l'option [JSON_THROW_ON_ERROR] (un paramètre nommé), un texte mal formé lève une exception [JsonException], que le bloc [try] / [catch] intercepte pour répondre par une erreur 400. La méthode [json()] de la requête ferait le même travail, mais, pour un JSON mal formé, rendrait simplement un résultat vide ;
  • ligne 36 : la méthode [input] cherche un paramètre partout : dans le corps de la requête, qu'il soit un formulaire ou du JSON, puis dans l'URL. C'est la méthode que l'on utilise le plus souvent, parce qu'elle ne dépend pas de la forme de la requête ;
  • ligne 42 : un champ du formulaire, lu par [input] ;
  • lignes 49-56 : les règles de la classe [PersonneRequest] de l'exemple 08, appliquées cette fois aux champs du formulaire. Le code est le même qu'avec les paramètres de l'URL : seule la source des données change. [sort] trie les messages, pour une réponse stable. L'opérateur ternaire [condition ? a : b] rend [a] si la condition est vraie, [b] sinon.

La table de routage déclare des routes POST, et [bootstrap/app.php] écarte la protection anti-CSRF pour ces URL, comme à l'exemple 02 :

<?php
// routes/web.php : les routes de l'exemple 11

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::prefix('post')->controller(PostController::class)->group(function () {
    Route::post('action01', 'action01');
    Route::post('action02', 'action02');
    Route::post('action03', 'action03');
    Route::post('action04', 'action04');
});
<?php
// bootstrap/app.php : l'application de l'exemple 11

use App\Http\Middleware\JsonLisible;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;

(require __DIR__.'/../../vendor/autoload.php')->addPsr4('App\\', __DIR__.'/../app/');

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(web: __DIR__.'/../routes/web.php')
    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->append(JsonLisible::class);
        // les requêtes POST envoyées par curl ne passeraient pas la protection
        // anti-CSRF (exemple 21)
        $middleware->preventRequestForgery(except: ['post/*']);
    })
    ->withExceptions()
    ->create();

Avec [curl], l'option [-d] (*data*) envoie un corps ; la requête devient alors une requête POST, de type [application/x-www-form-urlencoded], sauf si l'option [-H] (*header*) déclare un autre type. L'option [-X POST] fixe explicitement la méthode (elle est facultative ici). Les champs d'un formulaire, dont un champ répété :

curl -i -g -X POST -d "nom=martin&age=42&loisir[]=tennis&loisir[]=piano" http://localhost:8000/post/action01
HTTP/1.1 200 OK
Content-Type: application/json

{
    "typeRecu": "application/x-www-form-urlencoded",
    "champs": {
        "nom": "martin",
        "age": "42",
        "loisir": [
            "tennis",
            "piano"
        ]
    }
}

Un corps JSON, qui contient un entier et un tableau :

curl -i -H "Content-Type: application/json" -d "{\"nom\": \"martin\", \"age\": 42, \"enfants\": [\"léa\", \"tom\"]}" http://localhost:8000/post/action02
HTTP/1.1 200 OK
Content-Type: application/json

{
    "typeRecu": "application/json",
    "corps": {
        "nom": "martin",
        "age": 42,
        "enfants": [
            "léa",
            "tom"
        ]
    },
    "nom": "martin"
}

Les guillemets du JSON sont précédés d'un [\] pour ne pas terminer la chaîne de la commande ; sous [Linux] et [macOS], on peut aussi entourer le JSON d'apostrophes : [-d '{"nom": "martin"}']. Contrairement aux champs d'un formulaire, le JSON transporte des types : l'âge est déjà un entier. Un corps qui n'est pas du JSON, puis un JSON mal formé (en demandant une réponse JSON, pour voir le message) :

curl -i -H "Accept: application/json" -d "nom=martin" http://localhost:8000/post/action02
curl -i -H "Accept: application/json" -H "Content-Type: application/json" -d "{\"nom\": " http://localhost:8000/post/action02
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json

{
    "message": "le corps de la requête doit être du JSON",
    ...
}
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
    "message": "JSON mal formé : Syntax error",
    ...
}

Un champ unique, puis la même URL demandée en GET :

curl -d "nom=jean%20dupont" http://localhost:8000/post/action03
curl -i "http://localhost:8000/post/action03?nom=martin"
1
2
3
4
5
nom = [jean dupont]
HTTP/1.1 405 Method Not Allowed
allow: POST
Content-Type: text/html; charset=utf-8
...

L'URL existe, mais pas pour la méthode GET : [Laravel] répond 405 (*Method Not Allowed*), et son entête [allow] indique les méthodes acceptées. Enfin, les règles appliquées aux champs du formulaire, satisfaites puis non :

curl -d "nom=martin&age=42&titre=Mr" http://localhost:8000/post/action04
curl -d "nom=m&age=200" http://localhost:8000/post/action04
{
    "personne": {
        "nom": "martin",
        "age": "42",
        "titre": "Mr"
    },
    "valide": true,
    "erreurs": []
}
{
    "personne": null,
    "valide": false,
    "erreurs": [
        "age : la valeur doit être comprise entre 0 et 150",
        "nom : 2 caractère(s) au moins",
        "titre : le champ est obligatoire"
    ]
}

3.6. Exemple 12 : liaison et validation personnalisées

Les types et les règles standard ne suffisent pas toujours. Cet exemple montre trois façons d'étendre la liaison et la validation :

  • un type lié à un paramètre de route : une date au format français, [23-09-2026], dans le chemin de l'URL ;
  • un middleware qui fournit à l'action un paramètre qui ne vient pas de l'URL, mais d'un entête HTTP ;
  • une règle de validation « maison » (pas d'espace), et la conversion d'une date [jj/mm/aaaa].
12-liaison-perso/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   └── ExemplesController.php
│   │   └── Requests/
│   │       └── RechercheRequest.php
│   └── Liaison/
│       ├── DetecterLangue.php
│       ├── JourFr.php
│       └── SansEspace.php
├── lang/
│   └── fr/
│       └── validation.php
└── ...

3.6.1. Un type lié à un paramètre de route : app/Liaison/JourFr.php

Nous avons vu à l'exemple 02 qu'un paramètre de route est passé à l'action sous forme de chaîne, ou d'entier si le paramètre de l'action est de type [int]. [Laravel] sait aussi transformer un paramètre de route en objet, si l'action demande un paramètre dont la classe implémente l'interface [UrlRoutable]. C'est la liaison de route (*route model binding*), que [Laravel] utilise surtout pour les entités d'une base de données (un identifiant dans l'URL devient l'objet correspondant, chapitre 9). Nous l'utilisons ici pour une date.

<?php
// app/Liaison/JourFr.php : une date française "23-09-2026" dans le chemin de l'URL

namespace App\Liaison;

use Carbon\CarbonImmutable;
use Illuminate\Contracts\Routing\UrlRoutable;

// un type qui sait se LIER à un paramètre de route (resolveRouteBinding) et
// redevenir un morceau d'URL (getRouteKey) : l'interface UrlRoutable
class JourFr implements UrlRoutable
{
    public function __construct(public ?CarbonImmutable $date = null)
    {
    }

    // texte du chemin -> objet JourFr ; null : la valeur ne convient pas (erreur 404)
    public function resolveRouteBinding($valeur, $champ = null): ?static
    {
        // le ! de "!d-m-Y" met à zéro l'heure (sinon, ce serait l'heure courante)
        $date = CarbonImmutable::createFromFormat('!d-m-Y', $valeur);
        // createFromFormat accepte "31-02-2026" (et en fait le 3 mars) : on vérifie
        // que la date, remise en forme, redonne bien le texte reçu
        if ($date === null || $date->format('d-m-Y') !== $valeur) {
            return null;
        }
        return new static($date);
    }

    // objet JourFr -> texte du chemin (utilisé par la fonction route)
    public function getRouteKey(): string
    {
        return $this->date->format('d-m-Y');
    }

    public function getRouteKeyName(): string
    {
        return 'jour';
    }

    public function resolveChildRouteBinding($typeEnfant, $valeur, $champ): ?static
    {
        return null;
    }
}
  • ligne 11 : la classe implémente l'interface [UrlRoutable], dont les quatre méthodes suivent ;
  • ligne 13 : la date, un objet [CarbonImmutable]. [Carbon] est la bibliothèque de dates de [Laravel] ; elle enrichit la classe [DateTime] de [PHP] de centaines de méthodes. La version [Immutable] produit un nouvel objet à chaque modification ([$date->addDay()] rend une nouvelle date), au lieu de modifier l'objet. Le constructeur a une valeur par défaut, [null] : [Laravel] doit pouvoir créer un objet [JourFr] vide, dont il appellera ensuite la méthode [resolveRouteBinding] ;
  • lignes 18-27 : [resolveRouteBinding] transforme le texte du chemin en objet : elle rend l'objet lié, ou [null] si la valeur ne convient pas. [CarbonImmutable::createFromFormat] analyse une chaîne selon un format : [d] le jour, [m] le mois, [Y] l'année sur quatre chiffres ; le [!] initial met l'heure à zéro. [createFromFormat] est tolérant : il accepte [31-02-2026], et en fait le 3 mars. La vérification de la ligne 24 rejette ce cas : la date, remise en forme, doit redonner exactement le texte reçu. [new static(...)] crée un objet de la classe courante. Quand la méthode rend [null], [Laravel] répond par une erreur 404 : l'URL ne désigne rien ;
  • lignes 31-34 : l'opération inverse, utilisée par la fonction [route] pour construire une URL à partir d'un objet [JourFr]. [format] met une date en forme ;
  • lignes 36-41 : les deux dernières méthodes de l'interface, qui servent aux entités d'une base de données, n'ont pas de rôle ici.

3.6.2. Un paramètre fourni par un middleware : app/Liaison/DetecterLangue.php

Une action [Laravel] reçoit en paramètres les paramètres de route et les objets que le conteneur sait fournir (la requête, les services). Pour lui fournir une autre information, calculée à partir de la requête, on peut la faire calculer par un middleware (exemple 03), qui la range dans la requête, avant l'action :

<?php
// app/Liaison/DetecterLangue.php : un middleware qui CALCULE une information pour l'action

namespace App\Liaison;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class DetecterLangue
{
    public function handle(Request $request, Closure $next): Response
    {
        // l'entête "Accept-Language: fr-FR,fr;q=0.9,en;q=0.8" -> "fr" : la langue
        // préférée du navigateur parmi celles proposées (la première par défaut)
        $langue = $request->getPreferredLanguage(['fr', 'en']);
        // les ATTRIBUTS de la requête : des informations que l'application y range
        // pour les fonctions qui traiteront la requête après elle
        $request->attributes->set('langue', $langue);
        return $next($request);
    }
}
  • ligne 16 : l'entête [Accept-Language] de la requête indique les langues de l'utilisateur, par ordre de préférence : [fr-FR,fr;q=0.9,en;q=0.8] (le coefficient [q] mesure la préférence, de 0 à 1). [getPreferredLanguage] choisit, parmi les langues proposées par l'application, celle que l'utilisateur préfère ; sans entête, c'est la première de la liste ;
  • ligne 19 : la requête a des attributs : un tableau d'informations que l'application y range, pour les fonctions qui traiteront la requête après elle. On y range la langue, sous le nom [langue] ;
  • ligne 20 : la requête poursuit son chemin, vers l'action.

L'action recevra cette information grâce à un attribut [PHP] placé devant son paramètre (voir le contrôleur).

3.6.3. Une règle « maison » et une conversion : SansEspace.php et RechercheRequest.php

<?php
// app/Liaison/SansEspace.php : une règle de validation "maison"

namespace App\Liaison;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class SansEspace implements ValidationRule
{
    // la valeur ne convient pas : on appelle $echec avec le message d'erreur
    public function validate(string $attribut, mixed $valeur, Closure $echec): void
    {
        if (str_contains((string) $valeur, ' ')) {
            $echec("le champ ne doit pas contenir d'espace");
        }
    }
}
  • ligne 9 : une règle de validation « maison » est une classe qui implémente l'interface [ValidationRule] ;
  • lignes 12-17 : sa méthode [validate] reçoit le nom du champ, sa valeur, et une fonction, [$echec], qu'elle appelle, avec le message d'erreur, si la valeur ne convient pas. [str_contains] teste la présence d'un texte dans une chaîne ; [(string)] convertit la valeur en chaîne. Le type [mixed] désigne n'importe quel type.

La règle est utilisée par la requête de formulaire de la recherche :

<?php
// app/Http/Requests/RechercheRequest.php : le modèle de la recherche

namespace App\Http\Requests;

use App\Liaison\SansEspace;
use Carbon\CarbonImmutable;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

class RechercheRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            // une règle "maison" : un objet, dans une liste de règles
            'motCle' => ['nullable', new SansEspace()],
            // date_format : une date au format jj/mm/aaaa
            'depuis' => 'nullable|date_format:d/m/Y',
        ];
    }

    // la date, CONVERTIE en objet date (null si elle est absente)
    public function depuis(): ?CarbonImmutable
    {
        return $this->date('depuis', 'd/m/Y')?->toImmutable();
    }

    // la réponse à une validation ratée, pour CETTE classe seulement : le message
    // de la première erreur, en texte brut, avec le code 400
    protected function failedValidation(Validator $validateur): never
    {
        throw new HttpResponseException(response($validateur->errors()->first(), 400,
            ['Content-Type' => 'text/plain; charset=utf-8']));
    }
}
  • ligne 18 : les règles d'un champ peuvent être données dans un tableau plutôt que dans une chaîne ; c'est indispensable pour une règle « maison », qui est un objet ;
  • ligne 20 : la règle [date_format:d/m/Y] vérifie que la valeur est une date au format donné, et que cette date existe ;
  • lignes 25-28 : la conversion de la date : la méthode [date] d'une requête lit un paramètre et le convertit en date [Carbon], selon le format donné ; elle rend [null] pour un paramètre absent. L'opérateur [?->] appelle une méthode si l'objet n'est pas [null], et rend [null] sinon ; [toImmutable] convertit la date en [CarbonImmutable] ;
  • lignes 32-36 : la méthode [failedValidation], héritée de [FormRequest], est appelée quand la validation échoue ; celle de [FormRequest] lève l'exception [ValidationException] (exemple 08). Nous la redéfinissons, pour cette classe seulement : elle lève une [HttpResponseException], une exception qui porte une réponse toute faite, que [Laravel] envoie telle quelle. La réponse contient le premier message d'erreur ([first()]), en texte brut, avec le code 400. C'est un moyen de traiter les erreurs de validation d'une action autrement que celles du reste de l'application.

Le message de la règle [date_format] est francisé par le fichier [lang/fr/validation.php] :

1
2
3
4
5
6
<?php
// lang/fr/validation.php : les messages des erreurs de validation, en français
return [
    // :input : la valeur reçue
    'date_format' => "[:input] n'est pas une date jj/mm/aaaa valide",
];
  • ligne 5 : la marque [:input] est remplacée par la valeur reçue. Le texte contient une apostrophe : il est écrit entre guillemets.

3.6.4. Le contrôleur : app/Http/Controllers/ExemplesController.php

<?php
// app/Http/Controllers/ExemplesController.php : liaison et validation personnalisées

namespace App\Http\Controllers;

use App\Http\Requests\RechercheRequest;
use App\Liaison\JourFr;
use Illuminate\Container\Attributes\RequestAttribute;

class ExemplesController
{
    // /exemples/agenda/23-09-2026 : le paramètre de route {jour} est LIÉ à un objet
    // JourFr, parce que l'action demande un paramètre de ce type
    public function agenda(JourFr $jour): string
    {
        // route() utilise la méthode getRouteKey de JourFr : objet -> "23-09-2026"
        return "agenda du jour [{$jour->date->toDateString()}], de type "
            .get_debug_type($jour->date).', URL : '.route('agenda', $jour, false);
    }

    // /exemples/bonjour : un paramètre qui ne vient pas de l'URL, mais d'un entête,
    // calculé par le middleware DetecterLangue et rangé dans les attributs de la requête
    public function bonjour(#[RequestAttribute('langue')] string $langue): string
    {
        return $langue === 'en' ? 'Hello!' : 'Bonjour !';
    }

    // /exemples/recherche?motCle=laravel&depuis=01/09/2026
    public function recherche(RechercheRequest $request): string
    {
        $motCle = $request->validated('motCle');
        $depuis = $request->depuis()?->toDateString();
        return "recherche de [$motCle] depuis le [$depuis]";
    }
}
  • lignes 14-18 : le paramètre [$jour] est de type [JourFr] : [Laravel] lie le paramètre de route [{jour}] à un objet [JourFr], par sa méthode [resolveRouteBinding]. [toDateString()] écrit la date au format international ([2026-09-23]). La fonction [route] construit l'URL de cette même action à partir de l'objet, grâce à sa méthode [getRouteKey] ; son troisième paramètre, [false], demande une URL relative (sans le protocole ni la machine) ;
  • lignes 23-25 : le paramètre [$langue] est précédé d'un attribut [PHP], écrit [#[...]] : une information attachée au code, que [PHP] ignore mais que les bibliothèques lisent (l'équivalent des annotations de [Java]). L'attribut [RequestAttribute('langue')] demande au conteneur de fournir, pour ce paramètre, l'attribut [langue] de la requête, calculé par le middleware [DetecterLangue] ;
  • lignes 29-33 : l'action reçoit la requête de formulaire, déjà validée (exemple 08). [validated('motCle')] rend un seul champ validé ; [depuis()] rend la date convertie.

La table de routage :

<?php
// routes/web.php : les routes de l'exemple 12

use App\Http\Controllers\ExemplesController;
use App\Liaison\DetecterLangue;
use Illuminate\Support\Facades\Route;

Route::prefix('exemples')->controller(ExemplesController::class)->group(function () {
    // where : seul un morceau de chemin de la forme jj-mm-aaaa est proposé à JourFr
    Route::get('agenda/{jour}', 'agenda')->name('agenda')
        ->where('jour', '[0-9]{2}-[0-9]{2}-[0-9]{4}');
    // middleware : DetecterLangue s'exécute avant l'action
    Route::get('bonjour', 'bonjour')->middleware(DetecterLangue::class);
    Route::get('recherche', 'recherche');
});
  • lignes 10-11 : la route de l'agenda est nommée (pour la fonction [route]), et son paramètre est contraint par une expression régulière : [[0-9]] désigne un chiffre, [{2}] la répétition de ce qui précède : deux chiffres, un tiret, deux chiffres, un tiret, quatre chiffres. Un morceau de chemin d'une autre forme ne correspond pas à la route (404), sans même que [JourFr] soit consulté ;
  • ligne 13 : la méthode [middleware] associe un middleware à une route : il ne s'exécute que pour elle, avant l'action. Nous retrouverons ce procédé tout au long du cours : les middlewares qui exigent un utilisateur connecté, au chapitre sur l'authentification, sont associés aux routes de la même façon.

3.6.5. Exécution

Le type lié au paramètre de route :

curl "http://localhost:8000/exemples/agenda/23-09-2026"
curl -i "http://localhost:8000/exemples/agenda/31-02-2026"
1
2
3
4
agenda du jour [2026-09-23], de type Carbon\CarbonImmutable, URL : /exemples/agenda/23-09-2026
HTTP/1.1 404 Not Found
Content-Type: text/html; charset=utf-8
...

La date est affichée au format international, mais l'URL construite par [route] a retrouvé le format français. Le 31 février n'existe pas : 404. Une date au format international, [2026-09-23], n'a pas la forme décrite par l'expression régulière : 404 également.

Le middleware. L'option [-H] de [curl] fixe l'entête [Accept-Language] :

1
2
3
curl -H "Accept-Language: en-US,en;q=0.9,fr;q=0.8" http://localhost:8000/exemples/bonjour
curl -H "Accept-Language: fr-FR,fr;q=0.9" http://localhost:8000/exemples/bonjour
curl http://localhost:8000/exemples/bonjour
1
2
3
Hello!
Bonjour !
Bonjour !

Sans entête [Accept-Language] ([curl] n'en envoie pas), c'est la première langue de la liste. Le chapitre sur l'internationalisation reprendra cette question, avec un choix de langue fait par l'utilisateur.

La règle « maison » et la conversion de la date :

1
2
3
4
5
curl "http://localhost:8000/exemples/recherche?motCle=laravel&depuis=01/09/2026"
curl "http://localhost:8000/exemples/recherche"
curl -i "http://localhost:8000/exemples/recherche?motCle=laravel%20mvc"
curl "http://localhost:8000/exemples/recherche?depuis=2026-09-01"
curl "http://localhost:8000/exemples/recherche?depuis=31/02/2026"
1
2
3
4
5
6
7
8
recherche de [laravel] depuis le [2026-09-01]
recherche de [] depuis le []
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8

le champ ne doit pas contenir d'espace
[2026-09-01] n'est pas une date jj/mm/aaaa valide
[31/02/2026] n'est pas une date jj/mm/aaaa valide

3.6.6. Conclusion

Récapitulons les moyens dont dispose une action pour obtenir son modèle :

Paramètres de route
les paramètres de l'action, de même nom ; convertis par leur type ([int $id]), contraints par [whereNumber], [where]... ; liés à un objet par [UrlRoutable]
Paramètres de l'URL
[$request->query(...)], [integer], [array], [date]... ; un paramètre répété s'écrit [nom[]]
Champs d'un formulaire POST
[$request->post(...)], ou [$request->input(...)], qui cherche partout
Corps JSON
[$request->input(...)], [$request->json()], ou [json_decode($request->getContent())]
Entêtes
[$request->header(...)], et des méthodes qui les analysent, comme [getPreferredLanguage]
Validation
des règles : une [FormRequest] (validation avant l'action) ou [Validator::make] (validation par l'action) ; des règles « maison » ([ValidationRule])
Messages
les fichiers [lang/xx/validation.php], et la langue [APP_LOCALE]
Erreurs communes
[$exceptions->render(...)] dans [bootstrap/app.php] (toute l'application), [failedValidation] (une requête de formulaire)
Paramètre calculé
un middleware qui range une information dans les attributs de la requête, [#[RequestAttribute]]

Les formulaires HTML, eux, ont besoin de plus que la validation : il faut les afficher, les réafficher avec la saisie de l'utilisateur et ses erreurs, les protéger contre les requêtes forgées. C'est l'objet du chapitre suivant, une fois les vues introduites.