Middleware

Middleware wird allgemein verwendet, um Anfragen oder Antworten abzufangen. Zum Beispiel um vor der Ausführung des Controllers die Benutzeridentität zu überprüfen, wie z.B. beim Umleiten auf die Anmeldeseite, wenn der Benutzer nicht angemeldet ist, oder um einen bestimmten Header in die Antwort einzufügen. Auch zur statistischen Erfassung der Anteile von URI-Anfragen usw.

Middleware Zwiebelmodell


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

Middleware und Controller bilden ein klassisches Zwiebelmodell; Middleware ist ähnlich wie die Schichten einer Zwiebelhaut und der Controller ist das Zwiebelinnere. Wie im Bild dargestellt, durchquert die Anfrage die Middleware 1, 2 und 3, um den Controller zu erreichen, der eine Antwort zurückgibt. Diese Antwort durchquert dann in umgekehrter Reihenfolge die Middleware 3, 2 und 1 und wird schließlich an den Client zurückgegeben. Mit anderen Worten, in jedem Middleware können wir sowohl die Anfrage als auch die Antwort erhalten.

Anfrageabfangung

Manchmal möchten wir nicht, dass eine bestimmte Anfrage die Controller-Ebene erreicht. Wenn wir zum Beispiel in middleware2 feststellen, dass der aktuelle Benutzer nicht angemeldet ist, können wir die Anfrage direkt abfangen und eine Anmeldeantwort zurückgeben. Der Ablauf sieht dann ungefähr so aus:


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

Wie im Bild dargestellt, wird eine Anmeldeantwort generiert, wenn die Anfrage die middleware2 erreicht, und die Antwort durchquert middleware1, bevor sie an den Client zurückgegeben wird.

Middleware-Schnittstelle

Middleware muss das Interface Webman\MiddlewareInterface implementieren.

interface MiddlewareInterface
{
    /**
     * Verarbeite eine eingehende Serveranfrage.
     *
     * Verarbeitet eine eingehende Serveranfrage mit dem Ziel, eine Antwort zu erzeugen.
     * Wenn es nicht möglich ist, die Antwort selbst zu erzeugen, kann es an den bereitgestellten
     * Anfrage-Handler delegieren, dies zu tun.
     */
    public function process(Request $request, callable $handler): Response;
}

Das bedeutet, dass die Methode process implementiert werden muss. Die Methode process muss ein support\Response-Objekt zurückgeben. Standardmäßig wird dieses Objekt durch $handler($request) generiert (die Anfrage wird weiterhin das Zwiebelinnere durchqueren), es kann auch eine Antwort sein, die durch Helper-Funktionen wie response(), json(), xml(), redirect() usw. generiert wird (die Anfrage wird stoppen und nicht weiter in das Zwiebelinnere eindringen).

Anfragen und Antworten in Middleware erhalten

In Middleware können wir die Anfrage sowie die Antwort nach der Ausführung des Controllers erhalten, sodass die Middleware intern in drei Teile gegliedert ist.

  1. Anfragedurchquerungsphase, also die Phase vor der Anfragebearbeitung
  2. Controllerverarbeitungsphase, also die Phase der Anfragebearbeitung
  3. Antwortausgabenphase, also die Phase nach der Anfragebearbeitung

Die drei Phasen in der Middleware werden wie folgt dargestellt:

<?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 'Hier ist die Anfragedurchquerungsphase, also die Phase vor der Anfragebearbeitung';

        $response = $handler($request); // Fortsetzung zur Zwiebelkern durchqueren, bis die Antwort vom Controller erhalten wurde

        echo 'Hier ist die Antwortausgabenphase, also die Phase nach der Anfragebearbeitung';

        return $response;
    }
}

Beispiel: Authentifizierungs-Middleware

Erstellen Sie die Datei app/middleware/AuthCheckTest.php (bitte erstellen Sie das Verzeichnis selbst, falls es nicht existiert) wie folgt:

<?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 angemeldet, die Anfrage kann weiter in das Zwiebelinnere durchqueren
            return $handler($request);
        }

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

        // Die aufgerufene Methode benötigt eine Anmeldung
        if (!in_array($request->action, $noNeedLogin)) {
            // Anfrage abfangen und eine Umleitungsantwort zurückgeben, die Anfrage stoppt das Eindringen in das Zwiebelinnere
            return redirect('/user/login');
        }

        // Keine Anmeldung erforderlich, die Anfrage kann weiterhin in das Zwiebelinnere durchqueren
        return $handler($request);
    }
}

Neuen Controller erstellen: app/controller/UserController.php

<?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' => 'login ok']);
    }

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

Achtung
Die $noNeedLogin enthält die Methoden des aktuellen Controllers, die ohne Anmeldung aufgerufen werden können.

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

return [
    // Globale Middleware
    '' => [
        // ... Hier wurden andere Middleware weggelassen
        app\middleware\AuthCheckTest::class,
    ]
];

Mit der Authentifizierungs-Middleware können wir uns im Controller auf das Schreiben von Geschäftscode konzentrieren, ohne uns um die Benutzeranmeldung kümmern zu müssen.

Beispiel: CORS Middleware

Erstellen Sie die Datei app/middleware/AccessControlTest.php (bitte erstellen Sie das Verzeichnis selbst, falls es nicht existiert) wie folgt:

<?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 eine OPTIONS-Anfrage handelt, wird eine leere Antwort zurückgegeben, andernfalls wird die Anfrage die Zwiebel weiterhin durchqueren und eine Antwort erhalten
        $response = $request->method() == 'OPTIONS' ? response('') : $handler($request);

        // Fügen Sie relevante HTTP-Header für CORS in die Antwort ein
        $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;
    }
}

Hinweis
CORS kann OPTIONS-Anfragen erzeugen; wir möchten nicht, dass OPTIONS-Anfragen in den Controller gelangen, also geben wir für OPTIONS-Anfragen direkt eine leere Antwort zurück (response('')), um die Anfrage abzufangen.
Wenn Ihre Schnittstelle Routen benötigt, verwenden Sie Route::any(..) oder Route::add(['POST', 'OPTIONS'], ..) um sie festzulegen.

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

return [
    // Globale Middleware
    '' => [
        // ... Hier wurden andere Middleware weggelassen
        app\middleware\AccessControlTest::class,
    ]
];

Achtung
Wenn eine AJAX-Anfrage benutzerdefinierte Header hat, müssen Sie in der Middleware den Access-Control-Allow-Headers-Header für diesen benutzerdefinierten Header hinzufügen, andernfalls wird der Fehler Request header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response. ausgegeben.

Erläuterung

  • Middleware wird unterteilt in globale Middleware, Anwendungs-Middleware (Anwendungs-Middleware ist nur im Mehranwendungsmodus gültig, siehe Multiple Applications) und Routen-Middleware.
  • Die Konfigurationsdatei für Middleware befindet sich unter config/middleware.php.
  • Globale Middleware wird unter dem Schlüssel '' konfiguriert.
  • Anwendungs-Middleware wird unter dem spezifischen Anwendungsnamen konfiguriert, z.B.
return [
    // Globale Middleware
    '' => [
        app\middleware\AuthCheckTest::class,
        app\middleware\AccessControlTest::class,
    ],
    // API-Anwendungs-Middleware (Anwendungs-Middleware ist nur im Mehranwendungsmodus gültig)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

Controller Middleware und Methoden Middleware

Mithilfe von Annotationen können wir Middleware für einen bestimmten Controller oder eine bestimmte Methode eines Controllers festlegen.

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

Wir können Middleware für eine bestimmte Route oder eine Gruppe von Routen festlegen.
Zum Beispiel in der config/route.php die folgende Konfiguration hinzufügen:

<?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-Konstruktor-Parameter

Konfigurationsdateien unterstützen das direkte Instanziieren von Middleware oder anonymen Funktionen, sodass Parameter bequem über Konstruktoren an Middleware übergeben werden können.
Zum Beispiel kann in config/middleware.php auch wie folgt konfiguriert werden:

return [
    // Globale Middleware
    '' => [
        new app\middleware\AuthCheckTest($param1, $param2, ...),
        function(){
            return new app\middleware\AccessControlTest($param1, $param2, ...);
        },
    ],
    // API-Anwendungs-Middleware (Anwendungs-Middleware ist nur im Mehranwendungsmodus gültig)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

Ebenso kann Routen-Middleware Parameter durch Konstruktoren ü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, ...);
    },
]);

Verwendung der Parameter in Middleware-Beispiel:

<?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.
  • Wenn auf der gleichen Ebene mehrere Middleware vorhanden sind, werden diese in der tatsächlichen Konfigurationsreihenfolge ausgeführt.
  • 404-Anfragen lösen standardmäßig keine Middleware aus (können jedoch über Route::fallback(function(){})->middleware() Middleware hinzufügen).

Routenparameter an Middleware übergeben (route->setParams)

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

Middleware an Controller-Parameter übergeben

Manchmal benötigt der Controller Daten, die in der Middleware erzeugt wurden. In diesem Fall können wir die Eigenschaften des $request-Objekts hinzufügen, um Parameter an den Controller zu übergeben. 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 um die aktuellen Routeninformationen zu erhalten

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

Routen-Konfiguration

<?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 die Anfrage keine Route gefunden hat (außer der Standardroute), ist $request->route null
        // Angenommen, der Browser greift auf die Adresse /user/111 zu, dann werden die folgenden Informationen ausgegeben
        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);
    }
}

Achtung

Middleware zur Fehlerbehandlung

Im Verlauf der Geschäftsverarbeitung können möglicherweise Ausnahmen entstehen. In der Middleware können Sie mit $response->exception() die Ausnahme abrufen.

Routen-Konfiguration

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

Superglobale Middleware

Die globale Middleware des Hauptprojekts wirkt sich nur auf das Hauptprojekt aus und hat keinen Einfluss auf Anwendungs-Plugins. Manchmal möchten wir eine Middleware hinzufügen, die global wirkt und alle Plugins einbezieht; dafür können wir die superglobale Middleware verwenden.

In config/middleware.php wie folgt konfigurieren:

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

Hinweis
@ Superglobale Middleware kann nicht nur im Hauptprojekt konfiguriert werden, sondern auch in einem bestimmten Plugin konfiguriert werden. Zum Beispiel wenn plugin/ai/config/middleware.php die @ Superglobale Middleware konfiguriert wird, wird dies auch das Hauptprojekt und alle Plugins betreffen.

Middleware zu einem bestimmten Plugin hinzufügen

Manchmal möchten wir einem bestimmten Anwendungs-Plugin eine Middleware hinzufügen, möchten jedoch nicht den Plugin-Code ändern (da dies bei einem Upgrade überschrieben wird), dann können wir in dem Hauptprojekt Middleware dafür konfigurieren.

In config/middleware.php wie folgt konfigurieren:

return [
    'plugin.ai' => [], // Fügt der ai-Plugin Middleware hinzu
    'plugin.ai.admin' => [], // Fügt der ai-Plugins admin-Modul (plugin\ai\app\admin Verzeichnis) Middleware hinzu
];

Hinweis
Natürlich können ähnliche Konfigurationen auch in einem Plugin hinzugefügt werden, um andere Plugins zu beeinflussen, zum Beispiel wenn plugin/foo/config/middleware.php solche Konfigurationen hinzugefügt wird, wird dies das ai-Plugin beeinflussen.