Middleware

Le middleware est généralement utilisé pour intercepter les requêtes ou les réponses. Par exemple, il peut exécuter une vérification d'identité de l'utilisateur avant d'atteindre le contrôleur, rediriger l'utilisateur vers la page de connexion s'il n'est pas connecté, ou ajouter un certain en-tête dans la réponse. Par exemple, pour calculer la part de certaines requêtes uri, etc.

Modèle d'oignon du middleware


            ┌──────────────────────────────────────────────────────┐
            │                     middleware1                      │ 
            │     ┌──────────────────────────────────────────┐     │
            │     │               middleware2                │     │
            │     │     ┌──────────────────────────────┐     │     │
            │     │     │         middleware3          │     │     │        
            │     │     │     ┌──────────────────┐     │     │     │
            │     │     │     │                  │     │     │     │
   ── Reqeust ───────────────────> Controller ── Response ───────────────────────────> Client
            │     │     │     │                  │     │     │     │
            │     │     │     └──────────────────┘     │     │     │
            │     │     │                              │     │     │
            │     │     └──────────────────────────────┘     │     │
            │     │                                          │     │
            │     └──────────────────────────────────────────┘     │
            │                                                      │
            └──────────────────────────────────────────────────────┘

Le middleware et le contrôleur forment un classique modèle d'oignon, où le middleware ressemble à des couches de peau d'oignon et le contrôleur est le cœur de l'oignon. Comme illustré, la requête traverse les middlewares 1, 2 et 3 pour atteindre le contrôleur, qui renvoie une réponse, puis la réponse traverse à son tour les middlewares dans l'ordre 3, 2, 1 pour finalement être retournée au client. Cela signifie qu'à chaque niveau du middleware, nous pouvons accéder à la fois à la requête et à la réponse.

Interception de requête

Parfois, nous ne voulons pas qu'une requête atteigne le niveau du contrôleur. Par exemple, si nous détectons dans le middleware2 que l'utilisateur actuel n'est pas connecté, nous pouvons directement intercepter la requête et renvoyer une réponse de connexion. Ce processus ressemble à ce qui suit :


            ┌────────────────────────────────────────────────────────────┐
            │                         middleware1                        │ 
            │     ┌────────────────────────────────────────────────┐     │
            │     │                   middleware2                  │     │
            │     │          ┌──────────────────────────────┐      │     │
            │     │          │        middleware3           │      │     │       
            │     │          │    ┌──────────────────┐      │      │     │
            │     │          │    │                  │      │      │     │
   ── Reqeust ─────────┐     │    │    Controller    │      │      │     │
            │     │ Response │    │                  │      │      │     │
   <───────────────────┘     │    └──────────────────┘      │      │     │
            │     │          │                              │      │     │
            │     │          └──────────────────────────────┘      │     │
            │     │                                                │     │
            │     └────────────────────────────────────────────────┘     │
            │                                                            │
            └────────────────────────────────────────────────────────────┘

Comme illustré, après que la requête atteigne middleware2, une réponse de connexion est générée. La réponse traverse ensuite middleware2, puis middleware1, avant d'être retournée au client.

Interface du Middleware

Le middleware doit implémenter l'interface Webman\MiddlewareInterface.

interface MiddlewareInterface
{
    /**
     * Process an incoming server request.
     *
     * Processes an incoming server request in order to produce a response.
     * If unable to produce the response itself, it may delegate to the provided
     * request handler to do so.
     */
    public function process(Request $request, callable $handler): Response;
}

Cela signifie qu'il doit implémenter la méthode process, qui doit renvoyer un objet support\Response. Par défaut, cet objet est généré par $handler($request) (la requête continuera à traverser le cœur de l'oignon), ou il peut s'agir d'une réponse générée par les fonctions d'assistance response(), json(), xml(), redirect(), etc. (la requête s'arrête ici et ne traverse plus le cœur de l'oignon).

Accès à la requête et à la réponse dans le middleware

Dans le middleware, nous pouvons accéder à la requête ainsi qu'à la réponse après l'exécution du contrôleur. Ainsi, le middleware se divise en trois parties.

  1. Phase de passage de la requête, c'est-à-dire la phase avant le traitement de la requête
  2. Phase de traitement de la requête par le contrôleur
  3. Phase de sortie de la réponse, c'est-à-dire la phase après le traitement de la requête

Les trois phases sont représentées dans le middleware comme suit :

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Test implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        echo 'Ici, c\'est la phase de passage de la requête, donc avant le traitement de la requête';

        $response = $handler($request); // Continue à traverser le cœur de l'oignon jusqu'à l'obtention d'une réponse du contrôleur

        echo 'Ici, c\'est la phase de sortie de la réponse, donc après le traitement de la requête';

        return $response;
    }
}

Exemple : Middleware de vérification de l'identité

Créer le fichier app/middleware/AuthCheckTest.php (créez le répertoire si nécessaire) comme suit :

<?php
namespace app\middleware;

use ReflectionClass;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class AuthCheckTest implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        if (session('user')) {
            // Déjà connecté, la requête continue à traverser le cœur de l'oignon
            return $handler($request);
        }

        // Utiliser la réflexion pour déterminer quelles méthodes du contrôleur ne nécessitent pas de connexion
        $controller = new ReflectionClass($request->controller);
        $noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];

        // La méthode à laquelle on accède nécessite une connexion
        if (!in_array($request->action, $noNeedLogin)) {
            // Intercepter la requête, renvoyer une réponse de redirection, la requête arrête de traverser le cœur de l'oignon
            return redirect('/user/login');
        }

        // Pas besoin de connexion, la requête continue à traverser le cœur de l'oignon
        return $handler($request);
    }
}

Créer le contrôleur app/controller/UserController.php

<?php
namespace app\controller;
use support\Request;

class UserController
{
    /**
     * Méthodes qui ne nécessitent pas de connexion
     */
    protected $noNeedLogin = ['login'];

    public function login(Request $request)
    {
        $request->session()->set('user', ['id' => 10, 'name' => 'webman']);
        return json(['code' => 0, 'msg' => 'login ok']);
    }

    public function info()
    {
        return json(['code' => 0, 'msg' => 'ok', 'data' => session('user')]);
    }
}

Attention
$noNeedLogin enregistre les méthodes accessibles sans connexion pour le contrôleur actuel.

Dans config/middleware.php, ajouter le middleware global comme suit :

return [
    // Middleware global
    '' => [
        // ... autres middlewares omis ici
        app\middleware\AuthCheckTest::class,
    ]
];

Avec le middleware de vérification d'identité, nous pouvons alors nous concentrer sur l'écriture de code métier dans le niveau du contrôleur sans nous soucier de savoir si l'utilisateur est connecté.

Exemple : Middleware de contrôle d'accès

Créer le fichier app/middleware/AccessControlTest.php (créez le répertoire si nécessaire) comme suit :

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class AccessControlTest implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        // Si c'est une requête OPTIONS, renvoyez une réponse vide, sinon continuez à traverser le cœur de l'oignon et obtenez une réponse
        $response = $request->method() == 'OPTIONS' ? response('') : $handler($request);

        // Ajouter des en-têtes http relatifs au contrôle d'accès à la réponse
        $response->withHeaders([
            'Access-Control-Allow-Credentials' => 'true',
            'Access-Control-Allow-Origin' => $request->header('origin', '*'),
            'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
            'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
        ]);

        return $response;
    }
}

Astuce
Les requêtes cross-origin peuvent générer des requêtes OPTIONS, et nous ne voulons pas que les requêtes OPTIONS atteignent le contrôleur, donc pour celles-ci, nous renvoyons directement une réponse vide (response('')), ce qui intercepte la requête.
Si votre interface nécessite de définir des routes, utilisez Route::any(..) ou Route::add(['POST', 'OPTIONS'], ..) pour les configurer.

Dans config/middleware.php, ajouter le middleware global comme suit :

return [
    // Middleware global
    '' => [
        // ... autres middlewares omis ici
        app\middleware\AccessControlTest::class,
    ]
];

Attention
Si une requête ajax utilise un en-tête personnalisé, assurez-vous d'inclure cet en-tête personnalisé dans le champ Access-Control-Allow-Headers au sein du middleware, sinon vous rencontrerez une erreur Request header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response.

Explications

  • Les middleware sont divisés en middleware globaux, middleware d'application (uniquement fonctionnels en mode multi-application, voir multi-application), et middleware de route.
  • Le fichier de configuration des middleware se trouve dans config/middleware.php.
  • Les middleware globaux sont configurés sous la clé ''.
  • Les middleware d'application sont configurés sous le nom d'application spécifique, par exemple :
return [
    // Middleware global
    '' => [
        app\middleware\AuthCheckTest::class,
        app\middleware\AccessControlTest::class,
    ],
    // Middleware d'application API (uniquement fonctionnels en mode multi-application)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

Middleware de contrôleur et Middleware de méthode

À l'aide des annotations, nous pouvons définir un middleware pour un contrôleur ou pour une méthode spécifique d'un contrôleur.

<?php
namespace app\controller;
use app\middleware\Controller1Middleware;
use app\middleware\Controller2Middleware;
use app\middleware\Method1Middleware;
use app\middleware\Method2Middleware;
use support\annotation\Middleware;
use support\Request;

#[Middleware(Controller1Middleware::class, Controller2Middleware::class)]
class IndexController
{
    #[Middleware(Method1Middleware::class, Method2Middleware::class)]
    public function index(Request $request): string
    {
        return 'hello';
    }
}

Middleware de route

Nous pouvons définir un middleware pour une route spécifique ou pour un groupe de routes.
Par exemple, ajouter la configuration suivante dans config/route.php :

<?php
use support\Request;
use Webman\Route;

Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
    app\middleware\MiddlewareA::class,
    app\middleware\MiddlewareB::class,
]);

Route::group('/blog', function () {
   Route::any('/create', function () {return response('create');});
   Route::any('/edit', function () {return response('edit');});
   Route::any('/view/{id}', function ($r, $id) {response("view $id");});
})->middleware([
    app\middleware\MiddlewareA::class,
    app\middleware\MiddlewareB::class,
]);

Passage de paramètres dans le middleware via le constructeur

Le fichier de configuration supporte l'instanciation directe des middleware ou des fonctions anonymes, ce qui facilite le passage de paramètres au middleware via le constructeur.
Ainsi, config/middleware.php peut être configuré comme suit :

return [
    // Middleware global
    '' => [
        new app\middleware\AuthCheckTest($param1, $param2, ...),
        function(){
            return new app\middleware\AccessControlTest($param1, $param2, ...);
        },
    ],
    // Middleware d'application API (uniquement fonctionnels en mode multi-application)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

De même, les middleware de route peuvent également recevoir des paramètres via le constructeur, par exemple dans config/route.php :

Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
    new app\middleware\MiddlewareA($param1, $param2, ...),
    function(){
        return new app\middleware\MiddlewareB($param1, $param2, ...);
    },
]);

Exemple d'utilisation de paramètres dans le middleware :

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class MiddlewareA implements MiddlewareInterface
{
    protected $param1;

    protected $param2;

    public function __construct($param1, $param2)
    {
        $this->param1 = $param1;
        $this->param2 = $param2;
    }

    public function process(Request $request, callable $handler) : Response
    {
        var_dump($this->param1, $this->param2);
        return $handler($request);
    }
}

Ordre d'exécution des Middleware

  • L'ordre d'exécution des middleware est : middleware global -> middleware d'application -> middleware de contrôleur -> middleware de route -> middleware de méthode.
  • Lorsqu'il y a plusieurs middleware au même niveau, ils s'exécutent dans l'ordre spécifique de la configuration.
  • Les requêtes 404 ne déclenchent par défaut aucun middleware (mais peuvent toujours avoir des middleware ajoutés via Route::fallback(function(){})->middleware()).

Passage de paramètres de la route au middleware (route->setParams)

Configuration de la route config/route.php

<?php
use support\Request;
use Webman\Route;

Route::any('/test', [app\controller\IndexController::class, 'index'])->setParams(['some_key' =>'some value']);

Middleware (supposons que ce soit un middleware global)

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        // Par défaut, $request->route est null, il faut donc vérifier si $request->route est nul
        if ($route = $request->route) {
            $value = $route->param('some_key');
            var_export($value);
        }
        return $handler($request);
    }
}

Passage de données d'un middleware à un contrôleur

Parfois, le contrôleur peut avoir besoin d'utiliser des données qui proviennent du middleware. Dans ce cas, nous pouvons passer les données en ajoutant des propriétés à l'objet $request. Par exemple :

Middleware

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $request->data = 'some value';
        return $handler($request);
    }
}

Contrôleur :

<?php
namespace app\controller;

use support\Request;

class FooController
{
    public function index(Request $request)
    {
        return response($request->data);
    }
}

Récupération d'informations sur la route actuelle dans le middleware

Nous pouvons utiliser $request->route pour récupérer l'objet de route et appeler les méthodes appropriées pour obtenir des informations pertinentes.

Configuration de la route

<?php
use support\Request;
use Webman\Route;

Route::any('/user/{uid}', [app\controller\UserController::class, 'view']);

Middleware

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $route = $request->route;
        // Si la requête ne correspond à aucune route (à l'exception de la route par défaut), alors $request->route est null
        // Supposons que l'adresse du navigateur soit /user/111, cela affichera les informations suivantes
        if ($route) {
            var_export($route->getPath());       // /user/{uid}
            var_export($route->getMethods());    // ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD','OPTIONS']
            var_export($route->getName());       // user_view
            var_export($route->getMiddleware()); // []
            var_export($route->getCallback());   // ['app\\controller\\UserController', 'view']
            var_export($route->param());         // ['uid'=>111]
            var_export($route->param('uid'));    // 111 
        }
        return $handler($request);
    }
}

Attention

Gestion des exceptions dans le middleware

Des exceptions peuvent survenir lors de la manipulation des affaires. Dans le middleware, utilisez $response->exception() pour récupérer l'exception.

Configuration de la route

<?php
use support\Request;
use Webman\Route;

Route::any('/user/{uid}', function (Request $request, $uid) {
    throw new \Exception('exception test');
});

Middleware :

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $response = $handler($request);
        $exception = $response->exception();
        if ($exception) {
            echo $exception->getMessage();
        }
        return $response;
    }
}

Middleware ultra-global

Le middleware global du projet principal n'affecte que le projet principal et n'aura pas d'incidence sur les plugins d'application. Parfois, nous souhaitons ajouter un middleware qui affecte globalement tous les plugins, c'est là qu'intervient le middleware ultra-global.

Dans config/middleware.php, configurez comme suit :

return [
    '@' => [ // Ajouter un middleware global pour le projet principal et tous les plugins
        app\middleware\MiddlewareGlobl::class,
    ], 
    '' => [], // N'ajoute le middleware global que pour le projet principal
];

Astuce
Le middleware ultra-global @ peut être configuré non seulement dans le projet principal mais également dans un plugin spécifique. Par exemple, configurez le middleware ultra-global @ dans le fichier plugin/ai/config/middleware.php, il affectera donc également le projet principal et tous les plugins.

Ajouter un middleware pour un plugin spécifique

Parfois, nous voulons ajouter un middleware à un plugin d'application mais sans modifier le code du plugin (car les mises à jour pourraient écraser ces modifications), dans ce cas, nous pouvons le configurer dans le projet principal.

Dans config/middleware.php, configurez comme suit :

return [
    'plugin.ai' => [], // Ajouter un middleware au plugin ai
    'plugin.ai.admin' => [], // Ajouter un middleware au module admin du plugin ai (dans le répertoire plugin\ai\app\admin)
];

Astuce
Vous pouvez également ajouter une configuration similaire dans un plugin pour influencer d'autres plugins. Par exemple, en ajoutant de telles configurations dans plugin/foo/config/middleware.php, cela influencera le plugin ai.