Middleware

Los middlewares se utilizan generalmente para interceptar peticiones o respuestas. Por ejemplo, se puede realizar una verificación unificada de la identidad del usuario antes de ejecutar el controlador, como redirigir a la página de inicio de sesión si el usuario no ha iniciado sesión. También se puede añadir un header a la respuesta o realizar estadísticas sobre la proporción de solicitudes a una determinada URI, etc.

Modelo de cebolla de middleware


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

Los middlewares y los controladores forman un clásico modelo de cebolla. Los middlewares son como las capas de la cebolla, mientras que el controlador es el núcleo de la cebolla. Como se ilustra en la figura, la solicitud atraviesa los middlewares 1, 2, 3 como una flecha para llegar al controlador, que devuelve una respuesta. Luego, la respuesta sale a través de los middlewares en el orden 3, 2, 1 y finalmente se regresa al cliente. Esto significa que en cada middleware podemos acceder tanto a la solicitud como a la respuesta.

Interceptación de solicitudes

A veces, no queremos que una solicitud llegue a la capa del controlador. Por ejemplo, si en middleware2 detectamos que el usuario actual no ha iniciado sesión, podemos interceptar la solicitud directamente y devolver una respuesta de inicio de sesión. Este proceso se parece a lo siguiente:


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

Como se muestra, después de que la solicitud llega a middleware2, se genera una respuesta de inicio de sesión. La respuesta regresa a través de middleware2 hacia middleware1 y luego se devuelve al cliente.

Interfaz de middleware

El middleware debe implementar la interfaz Webman\MiddlewareInterface.

interface MiddlewareInterface
{
    /**
     * Procesa una solicitud entrante al servidor.
     *
     * Procesa una solicitud entrante al servidor para generar una respuesta.
     * Si no puede generar la respuesta por sí mismo, puede delegar
     * a la función de manejador de solicitudes proporcionada para hacerlo.
     */
    public function process(Request $request, callable $handler): Response;
}

Es decir, se debe implementar el método process, que debe devolver un objeto support\Response. Este objeto es generado por defecto usando $handler($request) (la solicitud continuará atravesando el núcleo de la cebolla), aunque también puede ser una respuesta generada por funciones auxiliares como response(), json(), xml(), redirect(), etc. (la solicitud se detendrá y no continuará a través del núcleo de la cebolla).

Obtención de solicitudes y respuestas en middleware

Dentro del middleware, podemos obtener tanto la solicitud como la respuesta del controlador. Por lo tanto, el middleware se puede dividir en tres partes:

  1. Etapa de cruce de la solicitud, es decir, la etapa antes del procesamiento de la solicitud.
  2. Etapa de procesamiento de la solicitud por el controlador.
  3. Etapa de salida de la respuesta, es decir, la etapa después de procesar la solicitud.

Las tres etapas se reflejan en el middleware de la siguiente manera:

<?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 'Esta es la etapa de cruce de la solicitud, es decir, antes del procesamiento de la solicitud';

        $response = $handler($request); // Continuar atravesando el núcleo de la cebolla hasta ejecutar el controlador y obtener una respuesta

        echo 'Esta es la etapa de salida de la respuesta, es decir, después del procesamiento de la solicitud';

        return $response;
    }
}

Ejemplo: Middleware de verificación de identidad

Crea el archivo app/middleware/AuthCheckTest.php (crea el directorio si no existe) como sigue:

<?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')) {
            // Ya ha iniciado sesión, la solicitud continúa atravesando el núcleo de la cebolla
            return $handler($request);
        }

        // Usar reflexión para obtener qué métodos del controlador no requieren inicio de sesión
        $controller = new ReflectionClass($request->controller);
        $noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];

        // Si el método al que se accede requiere inicio de sesión
        if (!in_array($request->action, $noNeedLogin)) {
            // Interceptar la solicitud y devolver una respuesta de redirección, la solicitud se detiene y no continúa al núcleo de la cebolla
            return redirect('/user/login');
        }

        // No se requiere inicio de sesión, la solicitud continúa atravesando el núcleo de la cebolla
        return $handler($request);
    }
}

Crea un controlador app/controller/UserController.php

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

class UserController
{
    /**
     * Métodos que no requieren inicio de sesión
     */
    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')]);
    }
}

Nota
$noNeedLogin contiene los métodos del controlador que se pueden acceder sin iniciar sesión.

En config/middleware.php, añade el middleware global como sigue:

return [
    // Middleware global
    '' => [
        // ... Se omiten otros middlewares
        app\middleware\AuthCheckTest::class,
    ]
];

Con el middleware de verificación de identidad, podemos centrarnos en escribir código de negocio en el nivel del controlador sin preocuparnos por si el usuario ha iniciado sesión.

Ejemplo: Middleware de solicitud CORS

Crea el archivo app/middleware/AccessControlTest.php (crea el directorio si no existe) como sigue:

<?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 es una solicitud OPTIONS, devolver una respuesta vacía; de lo contrario, continuar atravesando el núcleo de la cebolla y obtener una respuesta
        $response = $request->method() == 'OPTIONS' ? response('') : $handler($request);

        // Añadir headers http relacionados con CORS a la respuesta
        $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;
    }
}

Sugerencia
Las solicitudes CORS pueden generar solicitudes OPTIONS; no queremos que estas solicitudes lleguen al controlador, por lo que devolvemos directamente una respuesta vacía (response('')) para interceptar la solicitud.
Si tu API necesita definir rutas, utiliza Route::any(..) o Route::add(['POST', 'OPTIONS'], ..) para configurarlas.

En config/middleware.php, añade el middleware global como sigue:

return [
    // Middleware global
    '' => [
        // ... Se omiten otros middlewares
        app\middleware\AccessControlTest::class,
    ]
];

Nota
Si una solicitud AJAX define headers personalizados, asegúrate de incluir ese header personalizado en el campo Access-Control-Allow-Headers en el middleware; de lo contrario, se producirá el error Request header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response.

Explicación

  • Los middlewares se dividen en middleware globales, middlewares de aplicación (los middlewares de aplicación sólo son efectivos en modo multiaplicación, consulta Multiaplicación), y middlewares de rutas.
  • El archivo de configuración de los middlewares se encuentra en config/middleware.php.
  • Los middlewares globales se configuran en la clave ''.
  • Los middlewares de aplicación se configuran bajo el nombre específico de la aplicación, por ejemplo:
return [
    // Middleware global
    '' => [
        app\middleware\AuthCheckTest::class,
        app\middleware\AccessControlTest::class,
    ],
    // Middleware de aplicación API (los middlewares de aplicación sólo son efectivos en modo multiaplicación)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

Middlewares de controlador y métodos

Utilizando anotaciones, podemos establecer middlewares para un controlador específico o para un método en particular de un controlador.

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

Podemos establecer middlewares para una ruta específica o para un grupo de rutas.
Por ejemplo, en config/route.php, añade la siguiente configuración:

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

Paso de parámetros al constructor de middleware

El archivo de configuración soporta la instanciación directa de middlewares o funciones anónimas, permitiendo pasar parámetros al middleware a través del constructor. Por ejemplo, en config/middleware.php también se puede configurar de esta forma:

return [
    // Middleware global
    '' => [
        new app\middleware\AuthCheckTest($param1, $param2, ...),
        function(){
            return new app\middleware\AccessControlTest($param1, $param2, ...);
        },
    ],
    // Middleware de aplicación API (los middlewares de aplicación sólo son efectivos en modo multiaplicación)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

Del mismo modo, los middlewares de ruta también pueden recibir parámetros a través del constructor, por ejemplo en 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, ...);
    },
]);

Ejemplo de uso de parámetros dentro del 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);
    }
}

Orden de ejecución de middlewares

  • El orden de ejecución de los middlewares es middleware global -> middleware de aplicación -> middleware de controlador -> middleware de ruta -> middleware de método.
  • Cuando hay múltiples middlewares en el mismo nivel, se ejecutan en el orden real conforme a su configuración.
  • Las solicitudes 404 por defecto no activan ningún middleware (pero aún se pueden añadir middlewares usando Route::fallback(function(){})->middleware()).

Paso de parámetros del middleware a la ruta (route->setParams)

Configuración de ruta en config/route.php

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

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

Middleware (suponiendo que sea 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
    {
        // Por defecto, $request->route es null, por lo tanto, necesitamos verificar si $request->route está vacío
        if ($route = $request->route) {
            $value = $route->param('some_key');
            var_export($value);
        }
        return $handler($request);
    }
}

Paso de parámetros del middleware al controlador

A veces, el controlador necesita utilizar datos generados en el middleware. En este caso, podemos pasar parámetros al controlador añadiendo propiedades al objeto $request. Por ejemplo:

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

Controlador:

<?php
namespace app\controller;

use support\Request;

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

Obtener información de la ruta actual en el middleware

Se puede utilizar $request->route para obtener el objeto de ruta y llamar a los métodos correspondientes para obtener información relevante.

Configuración de ruta

<?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 solicitud no coincide con ninguna ruta (excluyendo la ruta por defecto), entonces $request->route es null
        // Supongamos que el navegador accede a /user/111, se imprimirá la siguiente información
        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);
    }
}

Nota

Captura de excepciones en middleware

Durante el procesamiento de la lógica del negocio, pueden ocurrir excepciones. Se puede utilizar $response->exception() en el middleware para obtener la excepción.

Configuración de ruta

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

Los middlewares globales de la aplicación principal solo afectan a la aplicación principal y no impactan en los plugins de aplicación. A veces, queremos agregar un middleware que afecte a todo el sistema, incluyendo todos los plugins, y para eso se puede usar un middleware superglobal.

En config/middleware.php, se configura de la siguiente manera:

return [
    '@' => [ // Añadir middleware global a la aplicación principal y todos los plugins
        app\middleware\MiddlewareGlobl::class,
    ], 
    '' => [], // Añadir middleware global solo a la aplicación principal
];

Sugerencia
El middleware superglobal @ no solo puede configurarse en la aplicación principal, sino también en algún plugin. Por ejemplo, si en el archivo de configuración de algún plugin, como plugin/ai/config/middleware.php, se incluye middleware superglobal @, también afectará a la aplicación principal y a todos los plugins.

Añadir middleware a un plugin

A veces, se desea añadir un middleware a algún plugin de aplicación sin modificar el código del plugin (porque se sobrescribirá durante la actualización), en este caso, se puede configurar un middleware en la aplicación principal.

En config/middleware.php, se configura de la siguiente manera:

return [
    'plugin.ai' => [], // Añadir middleware al plugin ai
    'plugin.ai.admin' => [], // Añadir middleware al módulo admin del plugin ai (directorio plugin\ai\app\admin)
];

Sugerencia
Claro que también se puede agregar una configuración similar en algún plugin para afectar a otros plugins. Por ejemplo, si se añade configuración como la anterior en plugin/foo/config/middleware.php, afectará al plugin ai.