Middleware

Миддлвэры обычно используются для перехвата запросов или ответов. Например, это может быть проверка идентичности пользователя перед выполнением контроллера (например, перенаправление на страницу входа, если пользователь не вошел в систему) или добавление заголовка в ответ. Также можно использовать для статистики, чтобы узнать, какую долю определенных uri занимает запрос.

Модель луковицы миддлвэров


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

Миддлвэры и контроллер составляют классическую модель луковицы, где миддлвэры представляют собой уровни луковицы, а контроллер — её сердцевину. Как показано на рисунке, запрос проникает через middleware1, 2 и 3, чтобы добраться до контроллера, который возвращает ответ. Затем ответ проходит обратно через миддлвэры в порядке 3, 2, 1 и, в конечном итоге, возвращается клиенту. То есть, в каждом миддлвэре мы можем получить как запрос, так и ответ.

Перехват запроса

Иногда мы не хотим, чтобы какой-либо запрос достигал уровня контроллера. Например, если в middleware2 мы обнаруживаем, что текущий пользователь не вошел в систему, мы можем напрямую перехватить запрос и вернуть ответ для входа. Этот процесс выглядит следующим образом:


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

Как видно на рисунке, после того как запрос достиг middleware2, он генерирует ответ для входа, и ответ возвращается от middleware2 обратно в middleware1, а затем возвращается клиенту.

Интерфейс миддлвэра

Миддлвэры должны реализовывать интерфейс 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;
}

Это означает, что необходимо реализовать метод process, который должен возвращать объект support\Response. По умолчанию этот объект создается с помощью $handler($request) (запрос продолжает проходить через сердцевину луковицы), но также может быть создан с помощью таких вспомогательных функций, как response(), json(), xml(), redirect() и т. д. (запрос останавливает продолжение прохождения через сердцевину луковицы).

Получение запроса и ответа в миддлвэре

В миддлвэре мы можем получить запрос, а также ответ, полученный после выполнения контроллера. Таким образом, в миддлвэре можно выделить три этапа:

  1. Этап прохождения запроса, т.е. перед обработкой запроса
  2. Этап обработки запроса контроллером, т.е. этап обработки запроса
  3. Этап возврата ответа, т.е. после обработки запроса

Эти три этапа в миддлвэре могут быть представлены следующим образом:

<?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 'Это этап прохождения запроса, то есть перед обработкой запроса';

        $response = $handler($request); // Продолжаем проходить через сердцевину луковицы, пока не получим ответ от контроллера

        echo 'Это этап возврата ответа, то есть после обработки запроса';

        return $response;
    }
}

Пример: миддлвэр для проверки идентификации

Создайте файл app/middleware/AuthCheckTest.php (если директория отсутствует, создайте её) со следующим содержимым:

<?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')) {
            // Успешный вход, продолжаем проходить через сердцевину луковицы
            return $handler($request);
        }

        // Используем рефлексию, чтобы определить, какие методы контроллера не требуют входа
        $controller = new ReflectionClass($request->controller);
        $noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];

        // Метод требует входа
        if (!in_array($request->action, $noNeedLogin)) {
            // Перехватываем запрос, возвращаем ответ перенаправления, останавливаем продолжение прохождения через сердцевину луковицы
            return redirect('/user/login');
        }

        // Не требуется входа, продолжаем проходить через сердцевину луковицы
        return $handler($request);
    }
}

Создайте контроллер app/controller/UserController.php

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

class UserController
{
    /**
     * Методы, для которых не требуется вход
     */
    protected $noNeedLogin = ['login'];

    public function login(Request $request)
    {
        $request->session()->set('user', ['id' => 10, 'name' => 'webman']);
        return json(['code' => 0, 'msg' => 'вход выполнен']);
    }

    public function info()
    {
        return json(['code' => 0, 'msg' => 'успешно', 'data' => session('user')]);
    }
}

Внимание
В $noNeedLogin указаны методы текущего контроллера, которые можно вызывать без входа

В файл config/middleware.php добавьте глобальный миддлвэр следующим образом:

return [
    // Глобальные миддлвэры
    '' => [
        // ... здесь пропущены другие миддлвэры
        app\middleware\AuthCheckTest::class,
    ]
];

С помощью миддлвэра для проверки идентификации мы можем сосредоточиться на написании бизнес-кода в контроллере, не беспокоясь о том, является ли пользователь вошедшим в систему.

Пример: миддлвэр для контроля доступа

Создайте файл app/middleware/AccessControlTest.php (если директория отсутствует, создайте её) со следующим содержимым:

<?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
    {
        // Если это запрос типа options, возвращаем пустой ответ, иначе продолжаем проходить через сердцевину луковицы и получаем ответ
        $response = $request->method() == 'OPTIONS' ? response('') : $handler($request);

        // Добавляем к ответу заголовки http, относящиеся к кросс-доменным запросам
        $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;
    }
}

Подсказка
Кросс-доменные запросы могут генерировать запросы типа OPTIONS, и мы не хотим, чтобы запросы типа OPTIONS достигли контроллера, поэтому мы непосредственно возвращаем пустой ответ (response('')), тем самым реализуя перехват запроса.
Если ваш интерфейс требует настройки маршрута, используйте Route::any(..) или Route::add(['POST', 'OPTIONS'], ..) для настройки.

В файл config/middleware.php добавьте глобальный миддлвэр следующим образом:

return [
    // Глобальные миддлвэры
    '' => [
        // ... здесь пропущены другие миддлвэры
        app\middleware\AccessControlTest::class,
    ]
];

Внимание
Если AJAX-запрос настроил пользовательские заголовки, необходимо добавить эти пользовательские заголовки в поле Access-Control-Allow-Headers в миддлвэре, иначе может возникнуть ошибка Request header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response.

Примечания

  • Миддлвэры делятся на глобальные миддлвэры, миддлвэры приложений (миддлвэры приложений действуют только в многоприложенческой модели, см. многоприложенческая модель) и маршрутизируемые миддлвэры.
  • Файл конфигурации миддлвэров находится в config/middleware.php.
  • Глобальные миддлвэры настраиваются по ключу ''.
  • Миддлвэры приложений настраиваются под конкретное имя приложения, например:
return [
    // Глобальные миддлвэры
    '' => [
        app\middleware\AuthCheckTest::class,
        app\middleware\AccessControlTest::class,
    ],
    // API миддлвэры приложений (миддлвэры приложений действуют только в многоприложенческой модели)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

Контроллерные миддлвэры и миддлвэры методов

С помощью аннотаций мы можем установить миддлвэры для конкретного контроллера или для определенного метода контроллера.

<?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 'привет';
    }
}

Маршрутизируемые миддлвэры

Мы можем установить миддлвэры для конкретного маршрута или группы маршрутов.
Например, добавьте следующую конфигурацию в 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('создать');});
   Route::any('/edit', function () {return response('редактировать');});
   Route::any('/view/{id}', function ($r, $id) {response("просмотр $id");});
})->middleware([
    app\middleware\MiddlewareA::class,
    app\middleware\MiddlewareB::class,
]);

Передача параметров в конструктор миддлвэра

Конфигурационный файл поддерживает непосредственную инстанциацию миддлвэров или анонимных функций, что позволяет удобно передавать параметры в миддлвэр через конструктор.
Например, config/middleware.php также может иметь следующую конфигурацию:

return [
    // Глобальные миддлвэры
    '' => [
        new app\middleware\AuthCheckTest($param1, $param2, ...),
        function(){
            return new app\middleware\AccessControlTest($param1, $param2, ...);
        },
    ],
    // API миддлвэры приложений (миддлвэры приложений действуют только в многоприложенческой модели)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

Аналогично, маршрутизируемые миддлвэры также могут передавать параметры через конструктор, например, в 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, ...);
    },
]);

Пример использования параметров в миддлвэре:

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

Порядок выполнения миддлвэров

  • Порядок выполнения миддлвэров таков: глобальные миддлвэры -> миддлвэры приложений -> контроллерные миддлвэры -> маршрутизируемые миддлвэры -> миддлвэры методов.
  • Когда на одном уровне несколько миддлвэров, они исполняются в порядке, установленном фактической конфигурацией.
  • Запросы с кодом 404 по умолчанию не вызывают никаких миддлвэров (тем не менее, можно добавить миддлвэр с помощью Route::fallback(function(){})->middleware()).

Передача параметров от маршрута к миддлвэру (route->setParams)

Конфигурация маршрута config/route.php

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

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

Миддлвэр (предполагается, что это глобальный миддлвэр)

<?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->route равен null, поэтому нужно проверять, является ли $request->route пустым
        if ($route = $request->route) {
            $value = $route->param('some_key');
            var_export($value);
        }
        return $handler($request);
    }
}

Передача параметров из миддлвэра в контроллер

Иногда контроллеру требуется использовать данные, созданные в миддлвэре. В этом случае мы можем передать параметры в контроллер, добавив свойство к объекту $request. Например:

Миддлвэр

<?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 = 'некоторое значение';
        return $handler($request);
    }
}

Контроллер:

<?php
namespace app\controller;

use support\Request;

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

Получение информации о текущем маршруте в миддлвэре

Мы можем использовать $request->route, чтобы получить объект маршрута, вызвав соответствующие методы для получения необходимой информации.

Конфигурация маршрута

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

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

Миддлвэр

<?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;
        // Если запрос не совпал с каким-либо маршрутом (кроме маршрута по умолчанию), то $request->route будет равен null
        // Предполагая, что браузер обращается по адресу /user/111, будет выведена следующая информация
        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);
    }
}

Внимание

Получение исключений в миддлвэре

В процессе обработки бизнес-логики могут возникать исключения, которые можно получить с помощью $response->exception() в миддлвэре.

Конфигурация маршрута

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

Route::any('/user/{uid}', function (Request $request, $uid) {
    throw new \Exception('Тест исключения');
});

Миддлвэр:

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

Супер-глобальные миддлвэры

Глобальные миддлвэры главного проекта влияют только на главный проект и не затрагивают плагины приложений. Иногда нам необходимо добавить миддлвэр, который будет оказывать влияние на все, включая все плагины, тогда можно использовать супер-глобальные миддлвэры.

В файле config/middleware.php настройте следующим образом:

return [
    '@' => [ // Добавить глобальный миддлвэр к главному проекту и всем плагинам
        app\middleware\MiddlewareGlobl::class,
    ], 
    '' => [], // Добавить глобальный миддлвэр только к главному проекту
];

Подсказка
Супер-глобальные миддлвэры можно настраивать не только в главном проекте, но и в любом из плагинов. Например, если вы добавите супер-глобальные миддлвэры в plugin/ai/config/middleware.php, это окажет влияние на главный проект и все плагины.

Добавление миддлвэров для конкретного плагина

Иногда мы хотим добавить миддлвэр для определенного плагина приложения, не изменяя код плагина (чтобы избежать перезаписи при обновлении), в этом случае мы можем настроить миддлвэр в главном проекте.

В файле config/middleware.php настройте следующим образом:

return [
    'plugin.ai' => [], // Добавить миддлвэр для плагина ai
    'plugin.ai.admin' => [], // Добавить миддлвэр для модуля admin плагина ai (каталог plugin\ai\app\admin)
];

Подсказка
Кроме того, вы можете добавить аналогичную конфигурацию в каком-либо из плагинов, чтобы повлиять на другие плагины. Например, добавив такую конфигурацию в plugin/foo/config/middleware.php, вы также повлияете на плагин ai.