Middleware

Middleware is commonly used to intercept requests or responses. For example, uniformly verify the user's identity before executing the controller, redirect to the login page if the user is not logged in, add a certain header to the response, and calculate the proportion of requests for a specific URI, and so on.

Middleware Onion Model

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

Middleware and controllers form a classic onion model, where the middleware is akin to the layers of an onion skin, and the controller is the core of the onion. As shown in the diagram, the request passes through middleware 1, 2, 3 to reach the controller like an arrow. The controller returns a response, and then the response passes through middleware in reverse order (3, 2, 1) before being returned to the client. This means that within each middleware, we can both access the request and receive the response.

Request Interception

Sometimes we do not want a certain request to reach the controller layer. For example, if we find in middleware 2 that the current user is not logged in, we can directly intercept the request and return a login response. In this case, the flow would look like the following:

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

As shown, when the request reaches middleware 2, it generates a login response, which then passes back through middleware 1 and is returned to the client.

Middleware Interface

Middleware must implement the Webman\MiddlewareInterface interface.

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;
}

This means that it must implement the process method, which must return a support\Response object. By default, this object is generated by $handler($request) (the request continues to pass through the onion core), but it can also be generated by helper functions such as response(), json(), xml(), redirect() which stop the request from continuing through the onion core.

Accessing Request and Response in Middleware

Within middleware, we can access both the request and the response after the controller has executed. Therefore, the middleware can be divided into three parts:

  1. Request passage phase, i.e., the phase before request processing.
  2. Controller processing request phase, i.e., request handling phase.
  3. Response passage phase, i.e., the phase after request processing.

The manifestation of these three phases in middleware is as follows:

<?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 'This is the request passage phase, i.e., before request processing.';

        $response = $handler($request); // Continue passing through the onion core until the controller generates a response

        echo 'This is the response passage phase, i.e., after request processing.';

        return $response;
    }
}

Beispiel: Authentifizierungsmiddleware

Erstellen Sie die Datei app/middleware/AuthCheckTest.php (erstellen Sie das Verzeichnis, falls es nicht vorhanden ist) wie unten gezeigt:

<?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')) {
            // Bereits eingeloggt, die Anfrage wird weiterhin durch die Zwiebelschichten gehen
            return $handler($request);
        }

        // Mit Reflexion die Methoden des Controllers ermitteln, die keine Anmeldung erfordern
        $controller = new ReflectionClass($request->controller);
        $noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];

        // Die aufgerufene Methode erfordert eine Anmeldung
        if (!in_array($request->action, $noNeedLogin)) {
            // Anfrage abfangen, eine Weiterleitungsantwort zurückgeben und die Anfrage stoppen, die Zwiebelschichten zu durchlaufen
            return redirect('/user/login');
        }

        // Keine Anmeldung erforderlich, die Anfrage wird weiterhin durch die Zwiebelschichten gehen
        return $handler($request);
    }
}

Erstellen Sie den Controller app/controller/UserController.php wie unten gezeigt:

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

class UserController
{
    /**
     * Methoden, die keine Anmeldung erfordern
     */
    protected $noNeedLogin = ['login'];

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

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

Fügen Sie im config/middleware.php globale Middleware wie folgt hinzu:

return [
    // Globale Middleware
    '' => [
        // ... andere Middleware hier ausgelassen
        app\middleware\AuthCheckTest::class,
    ]
];

Mit der Authentifizierungsmiddleware können wir uns auf die Geschäftslogik im Controller konzentrieren, ohne uns um die Benutzeranmeldung kümmern zu müssen.

Beispiel: Cross-Origin Request Middleware

Erstellen Sie die Datei app/middleware/AccessControlTest.php (erstellen Sie das Verzeichnis, falls es nicht vorhanden ist) wie unten gezeigt:

<?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
    {
        // Wenn es sich um einen OPTIONS-Request handelt, geben Sie eine leere Antwort zurück, ansonsten setzen Sie den Zwiebelschichtenprozess fort und erhalten eine Antwort
        $response = $request->method() == 'OPTIONS' ? response('') : $handler($request);

        // Fügen Sie der Antwort die relevanten CORS-Header hinzu
        $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;
    }
}

Fügen Sie im config/middleware.php globale Middleware wie folgt hinzu:

return [
    // Globale Middleware
    '' => [
        // ... andere Middleware hier ausgelassen
        app\middleware\AccessControlTest::class,
    ]
];

Wenn Ajax-Anfragen benutzerdefinierte Header verwenden, müssen Sie diese Header im Middleware-Feld Access-Control-Allow-Headers hinzufügen, da andernfalls ein Fehler auftritt: Request header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response.

Erklärung

  • Middleware wird in globale Middleware, Anwendungs-Middleware (gilt nur im Multi-App-Modus, siehe Multi-App), Routen-Middleware unterteilt
  • Derzeit werden keine Middleware für einzelne Controller unterstützt (es ist jedoch möglich, ähnliche Funktionen mittels Controller-Middleware zu implementieren, indem $request->controller überprüft wird)
  • Die Konfigurationsdatei für Middleware befindet sich unter config/middleware.php
  • Die Konfiguration für globale Middleware erfolgt unter dem Schlüssel ''
  • Die Konfiguration für Anwendungs-Middleware erfolgt unter dem spezifischen Anwendungsnamen, z.B.
return [
    // Globale Middleware
    '' => [
        app\middleware\AuthCheckTest::class,
        app\middleware\AccessControlTest::class,
    ],
    // Middleware für die API-Anwendung (gilt nur im Multi-App-Modus)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

Routen-Middleware

Sie können einer einzelnen Route oder einer Gruppe von Routen Middleware zuweisen. Zum Beispiel in config/route.php fügen Sie die folgende Konfiguration hinzu:

<?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,
]);

Middleware mit Konstruktorparametern übergeben

Hinweis
Diese Funktion erfordert webman-framework >= 1.4.8

Ab Version 1.4.8 unterstützt die Konfigurationsdatei das direkte Instanziieren von Middleware oder anonymen Funktionen, um Middleware-Parameter über den Konstruktor zu übergeben. Zum Beispiel können Sie config/middleware.php wie folgt konfigurieren:

return [
    // Globale Middleware
    '' => [
        new app\middleware\AuthCheckTest($param1, $param2, ...),
        function(){
            return new app\middleware\AccessControlTest($param1, $param2, ...);
        },
    ],
    // Middleware für die API-Anwendung (gilt nur im Multi-App-Modus)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

Ebenso können Sie Routen-Middleware verwenden, um Parameter über den Konstruktor zu übergeben. Zum Beispiel in 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, ...);
    },
]);

Ausführungsreihenfolge der Middleware

  • Die Ausführungsreihenfolge der Middleware ist globale Middleware -> Anwendungs-Middleware -> Routen-Middleware
  • Wenn mehrere globale Middleware vorhanden sind, werden sie in der tatsächlichen Konfigurationsreihenfolge ausgeführt (Anwendungs-Middleware und Routen-Middleware entsprechend)
  • 404-Anfragen aktivieren keine Middleware, einschließlich globaler Middleware

Parameter an Middleware übergeben (route->setParams)

Route-Konfiguration config/route.php

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

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

Middleware (als globale Middleware angenommen)

<?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
    {
        // Standardmäßig ist $request->route null, daher sollte überprüft werden, ob $request->route leer ist
        if ($route = $request->route) {
            $value = $route->param('some_key');
            var_export($value);
        }
        return $handler($request);
    }
}

Middleware-Parameter an Controller übergeben

Manchmal muss der Controller Daten verwenden, die im Middleware erzeugt wurden. In solchen Fällen können wir Parameter an den Controller übergeben, indem wir Eigenschaften dem $request-Objekt hinzufügen. Zum Beispiel:

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);
    }
}

Controller:

<?php
namespace app\controller;

use support\Request;

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

Middleware erhält aktuelle Anforderungs-Routing-Informationen

Hinweis
Erfordert webman-framework >= 1.3.2

Wir können $request->route verwenden, um das Routenobjekt zu erhalten und die entsprechenden Informationen durch Aufrufen der entsprechenden Methoden abzurufen.

Routenkonfiguration

<?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;
        // Wenn keine Route mit der Anfrage übereinstimmt (außer der Standardroute), ist $request->route null
        // Angenommen, der Browser ruft die Adresse /user/111 auf, dann wird die folgende Information gedruckt
        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);
    }
}

Hinweis
Die Methode $route->param() erfordert webman-framework >= 1.3.16

Middleware erhält Ausnahmen

Hinweis
Erfordert webman-framework >= 1.3.15

Während der Geschäftsabwicklung können Ausnahmen auftreten. Im Middleware können wir mit $response->exception() die Ausnahme abrufen.

Routenkonfiguration

<?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;
    }
}

Globale Middleware

Hinweis
Diese Funktion erfordert webman-framework >= 1.5.16

Die globalen Middleware des Hauptprojekts wirken sich nur auf das Hauptprojekt aus und haben keine Auswirkungen auf die Anwendungsplugins. Manchmal möchten wir jedoch eine Middleware hinzufügen, die sich global auf alle Plugins auswirkt. In diesem Fall können wir die globale Middleware verwenden.

Konfigurieren Sie in config/middleware.php wie folgt:

return [
    '@' => [ // Fügt dem Hauptprojekt und allen Plugins globale Middleware hinzu
        app\middleware\MiddlewareGlobl::class,
    ], 
    '' => [], // Fügt nur dem Hauptprojekt globale Middleware hinzu
];

Hinweis
Die @ globale Middleware kann nicht nur in der Hauptprojektkonfiguration verwendet werden, sondern auch in der Konfiguration eines Plugins. Wenn z. B. in plugin/ai/config/middleware.php die @ globale Middleware konfiguriert ist, wirkt sie sich auch auf das Hauptprojekt und alle Plugins aus.

Middleware für ein bestimmtes Plugin hinzufügen

Hinweis
Diese Funktion erfordert webman-framework >= 1.5.16

Manchmal möchten wir einem Anwendungsplugin Middleware hinzufügen, ohne den Code des Plugins zu ändern (weil er beim Upgrade überschrieben wird). In diesem Fall können wir dem Plugin in einem Hauptprojekt Middleware hinzufügen.

Konfigurieren Sie in config/middleware.php wie folgt:

return [
    'plugin.ai' => [], // Fügt dem Plugin "ai" Middleware hinzu
    'plugin.ai.admin' => [], // Fügt dem "admin"-Modul des Plugins "ai" Middleware hinzu
];

Hinweis
Natürlich können Sie auch in einem Plugin eine ähnliche Konfiguration hinzufügen, um andere Plugins zu beeinflussen. Wenn Sie beispielsweise in plugin/foo/config/middleware.php die obige Konfiguration hinzufügen, wirkt sie sich auf das Plugin "ai" aus.