Skip to content

7. Le cycle de vie d'une requête

Entre l'arrivée d'une requête et le départ de sa réponse, [Laravel] enchaîne plusieurs étapes, et l'application peut intervenir à chacune d'elles. Nous en avons déjà rencontré plusieurs : les middlewares (exemples 03, 12, 16, 22), la liaison des paramètres de route (exemple 12), les gestionnaires d'exceptions (exemples 08 et 12). Ce chapitre les réunit dans un seul exemple, qui affiche dans la console un message à chaque étape, pour en observer l'ordre :

  • 1 et 7. Les middlewares globaux, exécutés pour toutes les requêtes, avant même le routage. Un middleware enveloppe la suite du traitement : il voit passer la requête avant elle, et la réponse après elle (exemple 03). [Laravel] en installe plusieurs par défaut (les chaînes vides deviennent [null], le mode maintenance, les requêtes CORS...), dans l'ordre de la liste des middlewares globaux ;
  • Le routage. [Laravel] cherche la route qui correspond à l'URL et à la méthode. Si aucune ne correspond, une exception [NotFoundHttpException] est levée (404) ;
  • Les middlewares du groupe et de la route, dans l'ordre : d'abord ceux du groupe [web] (cookies, session, erreurs de formulaire, protection anti-CSRF, liaison des paramètres), puis ceux de la route. L'un d'eux, [SubstituteBindings], lie les paramètres de route (2, exemple 12). Les middlewares de la route (3 et 4) sont le bon endroit pour les vérifications propres à certaines actions : c'est ainsi que fonctionne le middleware [auth] du chapitre suivant. Si l'un d'eux rend une réponse sans appeler [$next] (ou lève une exception), le traitement s'arrête là : l'action n'est pas appelée ;
  • 5. L'action, et la vue qu'elle fait fabriquer ;
  • 6. La fin des middlewares, dans l'ordre inverse : chacun reçoit la réponse de [$next], peut la modifier (ajouter un entête, un cookie), et doit la rendre ;
  • 8. Les méthodes *[terminate] des middlewares qui en ont une, exécutées *après l'envoi de la réponse au navigateur. Elles ne peuvent plus changer la réponse : elles servent aux traitements qui peuvent attendre (écrire un journal, envoyer une statistique...), sans faire patienter l'utilisateur ;
  • x. Les gestionnaires d'exceptions : une exception levée par un middleware, par l'action ou par la vue est confiée au gestionnaire ([render], dans [bootstrap/app.php]) enregistré pour sa classe, qui fabrique une réponse d'erreur. Cette réponse remonte ensuite les middlewares, comme une réponse ordinaire.

C'est l'équivalent des filtres de [Java] et des intercepteurs de [Spring MVC], des filtres d'action d'[ASP.NET Core MVC], et des fonctions [before_request] / [after_request] de [Flask]. La particularité de [Laravel] est que presque tout y est middleware : la même notion sert au traitement de toutes les requêtes (middlewares globaux), d'une famille de routes (groupes), ou de quelques routes.

7.1. Exemple 24 : les étapes du traitement d'une requête

24-cycle-de-vie/
├── app/
│   └── Http/
│       ├── Controllers/
│       │   └── CalculController.php
│       └── Middleware/
│           ├── Bloquer.php
│           ├── Chrono.php
│           └── Journal.php
├── bootstrap/
│   └── app.php
├── resources/
│   └── views/
│       ├── calcul/
│       │   └── resultat.blade.php
│       ├── erreur.blade.php
│       └── layout.blade.php
├── routes/
│   └── web.php
└── ...

L'application n'a qu'une action, qui calcule le carré d'un entier : [/carre/7]. Selon l'URL, elle réussit, ou elle échoue de différentes façons. Les messages sont écrits dans la console du serveur par la fonction [error_log] de [PHP], que la commande [php artisan serve] affiche.

7.1.1. Le middleware global : app/Http/Middleware/Journal.php

<?php
// app/Http/Middleware/Journal.php : un middleware GLOBAL, le premier de tous : il
// voit passer chaque requête AVANT le reste de l'application, et la réponse APRÈS

namespace App\Http\Middleware;

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

class Journal
{
    public function handle(Request $request, Closure $next): Response
    {
        // error_log : un message dans la console du serveur de développement
        error_log('--- '.$request->method().' '.$request->getRequestUri());
        error_log('1. middleware global Journal (avant)');
        // toute la suite : les autres middlewares, le routage, l'action, la vue
        $reponse = $next($request);
        error_log('7. middleware global Journal (après : code '.$reponse->getStatusCode().')');
        return $reponse;
    }
}
  • lignes 16-17 : le middleware affiche la requête ([getRequestUri()] : le chemin et les paramètres). C'est le message n° 1 : rien ne s'est encore passé dans [Laravel], pas même le routage ;
  • ligne 19 : toute la suite du traitement se déroule dans cet appel ;
  • ligne 20 : le message n° 7, quand tout le reste a été fait. [getStatusCode()] est le code de la réponse.

Un middleware global ne connaît pas la route choisie, puisque le routage n'a pas encore eu lieu. On l'utilise pour des traitements qui concernent toutes les requêtes : journalisation, compression, prise en compte d'un proxy (le middleware [TrustProxies] de [Laravel] corrige l'adresse du client quand l'application est placée derrière un serveur mandataire ; l'étude de cas en parle, à propos de l'adresse IP des clients).

7.1.2. La liaison du paramètre : routes/web.php

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

use App\Http\Controllers\CalculController;
use App\Http\Middleware\Bloquer;
use App\Http\Middleware\Chrono;
use Illuminate\Support\Facades\Route;

// une LIAISON de paramètre : tout paramètre de route nommé {entier} est transformé
// par cette fonction avant d'être passé à l'action : un entier, ou null si ce n'est
// pas un nombre entier (c'est alors l'action qui décide)
Route::bind('entier', function (string $valeur) {
    error_log("2. liaison du paramètre (valeur reçue : \"$valeur\")");
    $n = filter_var($valeur, FILTER_VALIDATE_INT);
    return $n === false ? null : $n;
});

// la route : deux middlewares de route, exécutés dans cet ordre, avant l'action
Route::get('carre/{entier}', [CalculController::class, 'carre'])->name('carre')
    ->middleware([Bloquer::class, Chrono::class]);
  • lignes 12-16 : [Route::bind] enregistre une liaison explicite : tout paramètre de route nommé [{entier}] passe par cette fonction, qui reçoit le texte de l'URL et rend la valeur que recevra l'action. Elle accepte n'importe quel texte, et le convertit en entier, ou en [null] s'il n'en est pas un : contrairement à [whereNumber] (exemple 02), qui rendrait une page 404 pour [/carre/abc], c'est ici l'action qui décidera. Elle affiche le message n° 2. C'est l'autre forme de la liaison de route de l'exemple 12, qui liait un paramètre à une classe ([UrlRoutable]) ;
  • lignes 19-20 : la route, nommée [carre], reçoit deux middlewares de route, qui s'exécuteront dans cet ordre, après ceux du groupe [web].

7.1.3. Les middlewares de route : Bloquer.php et Chrono.php

<?php
// app/Http/Middleware/Bloquer.php : un middleware de ROUTE, exécuté pour les seules
// routes qui le demandent

namespace App\Http\Middleware;

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

class Bloquer
{
    public function handle(Request $request, Closure $next): Response
    {
        // $request->route() : la route choisie par le routage ; getName() : son nom
        error_log('3. middleware Bloquer (route : '.$request->route()->getName().')');
        if ($request->has('bloquer')) {
            // une exception : la suite (Chrono, l'action) n'est pas exécutée
            abort(403);
        }
        return $next($request);
    }
}
  • ligne 16 : message n° 3. Un middleware de route connaît la route choisie : [$request->route()] la rend, et [getName()] son nom ;
  • lignes 17-19 : le paramètre [?bloquer] de l'URL ([has] teste sa présence) provoque une erreur 403 (*Forbidden*, accès refusé) : [abort] lève une exception, qui interrompt le traitement.
<?php
// app/Http/Middleware/Chrono.php : un middleware de route, qui agit AVANT et APRÈS
// l'action, puis une dernière fois APRÈS l'envoi de la réponse (terminate)

namespace App\Http\Middleware;

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

class Chrono
{
    public function handle(Request $request, Closure $next): Response
    {
        error_log('4. middleware Chrono (avant)');
        // l'heure de départ (hrtime : une horloge précise, en nanosecondes), rangée
        // dans les attributs de la requête (portée requête, cf. exemple 12)
        $request->attributes->set('debut', hrtime(true));
        $reponse = $next($request);
        error_log('6. middleware Chrono (après : code '.$reponse->getStatusCode().')');
        return $reponse;
    }

    // terminate : appelée à la toute fin, APRÈS l'envoi de la réponse au navigateur ;
    // elle ne peut plus changer la réponse
    public function terminate(Request $request, Response $reponse): void
    {
        $debut = $request->attributes->get('debut');
        // la mesure n'a pas commencé si la requête a été bloquée avant ce middleware
        if ($debut !== null) {
            $ms = round((hrtime(true) - $debut) / 1e6);
            error_log("8. terminate Chrono (fin, $ms ms)");
        }
    }
}
  • lignes 15-18 : message n° 4, et l'heure de départ, rangée dans les attributs de la requête (exemple 12). [hrtime(true)] est une horloge précise, en nanosecondes, faite pour mesurer des durées ;
  • ligne 20 : message n° 6, après l'action, avec le code de la réponse ;
  • lignes 26-34 : la méthode [terminate], appelée par [Laravel] après l'envoi de la réponse, avec la requête et la réponse : message n° 8, avec la durée écoulée depuis le message n° 4. [Laravel] l'appelle pour tous les middlewares de la route, même s'ils n'ont pas été exécutés (quand un middleware précédent a interrompu le traitement) : le test de la ligne 30 vérifie que la mesure a commencé.

7.1.4. La gestion des erreurs : bootstrap/app.php

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

use App\Http\Middleware\Journal;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;

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

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        // la route qui sert Bootstrap (cf. exemple 15)
        then: fn () => require __DIR__.'/../../commun/bootstrap.php')
    ->withMiddleware(function (Middleware $middleware): void {
        // prepend : le middleware global Journal passe AVANT tous les autres
        $middleware->prepend(Journal::class);
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        // une exception "imprévue" d'un type donné : DomainException et ses sous-classes
        $exceptions->render(function (DomainException $e) {
            error_log("x. render DomainException ({$e->getMessage()})");
            // au lieu de la page d'erreur par défaut, notre vue [erreur], avec le code 500
            return response()->view('erreur', ['code' => 500], 500);
        });
        // les erreurs HTTP (abort(400), abort(403), page inconnue -> 404...) :
        // HttpExceptionInterface est l'interface de toutes les exceptions HTTP
        $exceptions->render(function (HttpExceptionInterface $e) {
            error_log("x. render HttpException (code {$e->getStatusCode()})");
            return response()->view('erreur', ['code' => $e->getStatusCode()], $e->getStatusCode());
        });
    })
    ->create();
  • ligne 19 : [prepend] place le middleware global en tête de la liste : il s'exécute avant tous les autres, et voit la réponse après tous les autres ;
  • lignes 23-27 : un gestionnaire pour les exceptions de la classe [DomainException] (et de ses sous-classes, cf. exemple 08) : une erreur « imprévue », transformée en page d'erreur « maison », avec le code 500 (*Internal Server Error*). Le message de l'exception est affiché dans la console, mais pas dans la page : il peut contenir des détails internes, qui ne regardent pas l'utilisateur ;
  • lignes 30-33 : un gestionnaire pour toutes les erreurs HTTP. [abort(400)], [abort(403)], une URL sans route (404), une méthode refusée (405) : toutes lèvent une exception qui implémente l'interface [HttpExceptionInterface] de [Symfony], et qui porte son code ([getStatusCode()]). Ce seul gestionnaire remplace donc toutes les pages d'erreur en anglais de [Laravel] par la nôtre.

[Laravel] confie une exception au premier gestionnaire dont le type du paramètre lui correspond. Une exception qui n'a pas de gestionnaire devient une erreur 500 : en mode debug, c'est la page détaillée (exemple 03) ; sans le mode debug, la page d'erreur 500 standard. Il existe une autre façon, plus simple encore, de remplacer les pages d'erreur : une vue [resources/views/errors/404.blade.php] remplace la page de l'erreur 404, et de même pour chaque code (l'exemple 26 s'en sert). Les gestionnaires [render], eux, permettent un traitement quelconque : un message dans le journal, une réponse JSON, une redirection...

7.1.5. Le contrôleur et la page d'erreur

<?php
// app/Http/Controllers/CalculController.php : une action protégée par deux middlewares

namespace App\Http\Controllers;

use DomainException;
use Illuminate\View\View;

class CalculController
{
    // $n : le paramètre de route {entier}, lié par Route::bind (cf. routes/web.php) ;
    // un paramètre lié à null n'est pas transmis à l'action : d'où la valeur par défaut
    public function carre(?int $n = null): View
    {
        error_log("5. action carre($n)");
        if ($n === null) {
            abort(400);    // une erreur 400 : cf. bootstrap/app.php
        }
        if ($n === 13) {
            throw new DomainException('13 porte malheur');    // une erreur « imprévue »
        }
        return view('calcul.resultat', ['n' => $n, 'carre' => $n * $n]);
    }
}
  • ligne 13 : le paramètre [$n] reçoit la valeur liée par [Route::bind]. [Laravel] ne transmet pas à l'action un paramètre lié à [null] : le paramètre de l'action a donc une valeur par défaut, [null], qui sera la sienne dans ce cas ;
  • ligne 17 : un paramètre qui n'est pas un entier : erreur 400 ;
  • ligne 20 : le nombre 13 provoque une erreur « imprévue ». [DomainException] est une classe d'exception de la bibliothèque standard de [PHP] ;
  • ligne 22 : le cas normal.
@extends('layout')
{{-- la page d'erreur ; son modèle : le code d'erreur --}}

@section('contenu')
  <div class="alert alert-danger">
    <h1 class="h4">Erreur {{ $code }}</h1>
    <p class="mb-0">
      @switch($code)
        @case(400) Requête incorrecte. @break
        @case(403) Accès refusé. @break
        @case(404) Page introuvable. @break
        @default Erreur interne du serveur.
      @endswitch
    </p>
  </div>
@endsection

La page d'erreur reçoit le code d'erreur, et affiche un message adapté. La directive [@switch] choisit un bloc selon une valeur : [@case] désigne une valeur, [@break] termine le bloc, [@default] traite les autres valeurs. La vue [calcul/resultat.blade.php] affiche simplement le résultat.

7.1.6. Exécution

Le cas normal, [/carre/7] :

Image

La console du serveur montre les huit étapes, dans l'ordre :

  --- GET /carre/7
  1. middleware global Journal (avant)
  2. liaison du paramètre (valeur reçue : "7")
  3. middleware Bloquer (route : carre)
  4. middleware Chrono (avant)
  5. action carre(7)
  6. middleware Chrono (après : code 200)
  7. middleware global Journal (après : code 200)
  8. terminate Chrono (fin, 5 ms)
  2026-09-26 18:19:51 /carre/7 ..................................... ~ 0.35ms

La dernière ligne est le journal du serveur de développement, écrit une fois la requête terminée. Un accès bloqué, [/carre/7?bloquer] :

Image

1
2
3
4
5
6
  --- GET /carre/7?bloquer
  1. middleware global Journal (avant)
  2. liaison du paramètre (valeur reçue : "7")
  3. middleware Bloquer (route : carre)
  x. render HttpException (code 403)
  7. middleware global Journal (après : code 403)

Le premier middleware de route a levé une exception : le second middleware et l'action n'ont pas été exécutés. La réponse d'erreur, fabriquée par le gestionnaire, remonte jusqu'au middleware global. [terminate] a été appelée, mais n'a rien affiché, la mesure n'ayant pas commencé. Un paramètre qui n'est pas un entier, [/carre/abc] :

Image

  --- GET /carre/abc
  1. middleware global Journal (avant)
  2. liaison du paramètre (valeur reçue : "abc")
  3. middleware Bloquer (route : carre)
  4. middleware Chrono (avant)
  5. action carre()
  x. render HttpException (code 400)
  6. middleware Chrono (après : code 400)
  7. middleware global Journal (après : code 400)
  8. terminate Chrono (fin, 6 ms)

Cette fois, l'action a été appelée, avec [null] (qui s'écrit comme une chaîne vide : [carre()]), et c'est elle qui a levé l'erreur 400. Une erreur « imprévue », [/carre/13] :

Image

  --- GET /carre/13
  1. middleware global Journal (avant)
  2. liaison du paramètre (valeur reçue : "13")
  3. middleware Bloquer (route : carre)
  4. middleware Chrono (avant)
  5. action carre(13)
  x. render DomainException (13 porte malheur)
  6. middleware Chrono (après : code 500)
  7. middleware global Journal (après : code 500)
  8. terminate Chrono (fin, 3 ms)

L'exception a été traitée par son gestionnaire : l'utilisateur voit la page d'erreur « maison », et non la page détaillée, bien que le mode debug soit actif. L'exception est tout de même écrite dans le journal [storage/logs/laravel.log] : [Laravel] distingue le rapport d'une exception (*report*, son enregistrement dans le journal) de son rendu (*render*, la réponse). Enfin, une URL qui ne correspond à aucune route, [/inconnu] :

1
2
3
4
  --- GET /inconnu
  1. middleware global Journal (avant)
  x. render HttpException (code 404)
  7. middleware global Journal (après : code 404)

Le routage a échoué : aucune liaison, aucun middleware de route, aucune action. La page 404 est la nôtre :

curl http://localhost:8000/inconnu
1
2
3
4
5
6
7
...
        <div class="alert alert-danger">
    <h1 class="h4">Erreur 404</h1>
    <p class="mb-0">
       Page introuvable.     </p>
  </div>
...

Retenons l'essentiel. Les middlewares de route sont le bon endroit pour les vérifications communes à de nombreuses actions (l'utilisateur est-il connecté ? a-t-il le droit d'accéder à cette action ?) : le chapitre suivant s'en sert pour l'authentification. Les gestionnaires d'exceptions et les vues [errors/...] garantissent que l'utilisateur ne voit jamais une page technique, quelle que soit l'erreur : l'étude de cas en tire parti.