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

Tipp
CORS kann OPTIONS-Preflight-Anfragen auslösen. Diese sollen den Controller nicht erreichen, daher wird für OPTIONS-Anfragen direkt eine leere Antwort (response('')) zur Anfrageunterbrechung zurückgegeben.
Wenn Ihre API ein Routing benötigt, verwenden Sie Route::any(..) oder Route::add(['POST', 'OPTIONS'], ..).

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

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

Hinweis
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
  • 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,
    ]
];

Controller-Middleware und Methoden-Middleware

Mit Annotations können wir einer bestimmten Controller-Klasse oder einer bestimmten Methode Middleware zuweisen.

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

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

Die Konfigurationsdatei unterstützt die direkte Instanziierung von Middleware, wodurch Parameter bequem über den Konstruktor übergeben werden können.
Zum Beispiel können Sie config/middleware.php wie folgt konfigurieren:

return [
    // Globale Middleware
    '' => [
        new app\middleware\AuthCheckTest($param1, $param2, ...),
        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, ...),
    new app\middleware\MiddlewareB($param1, $param2, ...),
]);

Beispiel für die Verwendung von Parametern in der 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);
    }
}

Ausführungsreihenfolge der Middleware

  • Die Ausführungsreihenfolge der Middleware ist globale Middleware -> Anwendungs-Middleware -> Controller-Middleware -> Routen-Middleware -> Methoden-Middleware
  • Bei mehreren Middleware auf derselben Ebene werden sie in der tatsächlichen Konfigurationsreihenfolge ausgeführt.
  • 404-Anfragen aktivieren standardmäßig keine Middleware (Sie können jedoch über Route::fallback(function(){})->middleware() Middleware hinzufügen).

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

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

Middleware erhält Ausnahmen

Während der Geschäftsabwicklung können Ausnahmen auftreten. In der 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;
    }
}

Super-Globale Middleware

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

Tipp
Die @ Super-Global-Middleware kann nicht nur im Hauptprojekt, sondern auch in einem Plugin konfiguriert werden. 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

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" (plugin\ai\app\admin) Middleware hinzu
];

Tipp
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.