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()
и т. д. (запрос останавливает продолжение прохождения через сердцевину луковицы).
Получение запроса и ответа в миддлвэре
В миддлвэре мы можем получить запрос, а также ответ, полученный после выполнения контроллера. Таким образом, в миддлвэре можно выделить три этапа:
- Этап прохождения запроса, т.е. перед обработкой запроса
- Этап обработки запроса контроллером, т.е. этап обработки запроса
- Этап возврата ответа, т.е. после обработки запроса
Эти три этапа в миддлвэре могут быть представлены следующим образом:
<?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.