Посредники

16 мая 2018 г.

Посредники (middleware) позволяют отфильтровать по каким-то признакам HTTP-запросы. Например, есть посредник, позволяющий пустить дальше запрос только от аутентифицированного пользователя, а остальных отправить на страницу логина; посредник CORS позволяет добавить заголовки к ответу приложения; посредник логирования записывает все запросы и т.д.

Посредники находятся в app/Http/Middleware. Из коробки их идет 5 штук.

Создать нового посредника можно командой make:middleware. Например, создадим посредника, проверяющего параметр age запроса, и пропускающего запрос только если age > 200; иначе запрос перенаправляется на главную страницу.

php artisan make:middleware CheckAge

Теперь откроем появившийся после этой команды файл app/Http/Middleware/CheckAge.php и внесем необходимые изменения в метод handle.

<?php

namespace App\Http\Middleware;

use Closure;

class CheckAge
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ($request->age <= 200) {
            return redirect('/');
        }

        return $next($request);
    }
}

Если параметр age  > 200, то с помощью внедренного в переменную $next коллбэка запрос передается дальше, либо к следующему в очереди посреднику, либо, если такого больше нет, далее на обработку приложением.

Теперь мы должны применить нашего посредника к тому роуту (либо группе роутов), где он необходим.

Route::get('oldman', function() {
    return 'Приветствую тебя, о аксакал!';
})->middleware(App\Http\Middleware\CheckAge::class);

Теперь, зайдя в браузере на http://laravel/oldman?age=201, получим приветствие. А если на http://laravel/oldman?age=200 - будем перенаправлены на главную.

Как видим, при привязке посредника к роуту мы указали полный класс. Но можно зарегистрировать псоредника в App\Http\Kernel, дописав его в массив $routeMiddleware

protected $routeMiddleware = [
    // ...
    'check-age' => \App\Http\Middleware\CheckAge::class
];

А в роуте привязать по зарегистрированному здесь ключу.

Route::get('oldman', function() {
    return 'Приветствую тебя, о аксакал!';
})->middleware('check-age');

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

Route::get('/', function() {
    return 'Приветствую тебя, о аксакал!';
})->middleware('first', 'second);

Если же посредника нужно привязать глобально (для всех HTTP-запросов к приложению), то нужно лишь добавить класс посредника в массив $middleware. Причем больше нигде ничего прописsвать не нужно. Но будьте осторожны с редиректами! Если привязать туда, например, нашего \App\Http\Middleware\CheckAge::class, то получим бесконечную переадресацию.

Глядя на массив $routeMiddleware, в котором уже из коробки присутствуют несколько посредников, легко понять, как просто можно, например, разрешить доступ к определенной странице лишь аутентифицированным юзерам.

Route::get('secret-page', function () {
    //
})->middleware('auth');

Во всех вышеприведенных примерах посредники выполнялись до обработки запроса приложением. Но легко сделать, чтобы это случилось после обработки запроса, нужно лишь прописать логику посредника в методе handle после вызова $next($request).

    public function handle($request, Closure $next)
    {
        $response = $next($request);

        // Здесь вся логика

        return $response;
    }

Наконец, третье свойство (хотя по-порядку оно второе) класса App\Http\Kernel - $middlewareGroups - тоже массив, позволяющий сгруппировать посредников для удобства назначения их роутам. Из коробки в Laravel есть две группы: web и api. Группы посредников назначаются роутам тем же синтаксисом, что и единичные посредники (из чего следует необходимость следить за уникальностью ключей сразу в двух свойствах App\Http\Kernel:  $middlewareGroups и $routeMiddleware).

Route::get('/', function () {
    //
})->middleware('web');

Route::group(['middleware' => ['web']], function () {
    //
});

Заметим, что группа посредников web из коробки назначена в RouteServiceProvider всему файлу роутов routes/web.php, а группа api - файлу routes/api.php.

Параметры посредников

Посредники могут принимать параметры. Например, если нам нужно проверить не просто аутентифицирован ли юзер, а и его роль (что естественно для доступа в админку, например), мы можем создать посредника CheckRole с передачей ему параметра role.

<?php

namespace App\Http\Middleware;

use Closure;

class CheckRole
{
    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string  $role
     * @return mixed
     */
    public function handle($request, Closure $next, $role)
    {
        if (! $request->user()->hasRole($role)) {
            // Redirect...
        }

        return $next($request);
    }

}

Параметры (любое количество) передаются в метод handle начиная с третьего, после $next.

А в роуте определяем требуемую роль следующим образом.

Route::put('post/{id}', function ($id) {
    //
})->middleware('check_role:editor');

Здесь предполагается, что мы сохранили посредника CheckRole под ключом check_role. Именно это имя является первой частью передаваемой в метод middleware строки. А через двоеточие идут значения параметров (если несколько, то через запятую, в том же порядке, в котором принимаются соответствующие аргументы в методе handle посредника).

Граничные посредники.

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

<?php

namespace Illuminate\Session\Middleware;

use Closure;

class StartSession
{
    public function handle($request, Closure $next)
    {
        return $next($request);
    }

    public function terminate($request, $response)
    {
        // Сохранение сессии...
    }
}

Этот метод принимает и запрос, и ответ ($response).

Важное замечание: при вызове метода terminate Laravel всегда обрабатывает новый экземпляр посредника из сервис-контейнера. Если необходимо, чтобы оба метода, handle и terminate, обрабатывались в одном экземпляре посредника, то регистрируем посредника в сервис-провайдере как singleton. Например, в AppServiceProvider.

public function register()
    {
        // ...
        $this->app->singleton(\App\Http\Middleware\TestTerminableMiddleware::class, function($app) {
            return new \App\Http\Middleware\TestTerminableMiddleware();
        });
    }