Як налаштувати фільтри "до" та "після"

Дата оновлення перкладу 2022-11-20

Як налаштувати фільтри "до" та "після"

У розробці вед-додатків достатньо часто бувають такі ситуації, коли деяку логіку потрібно виконати прямо до чи одразу після ваших дій контролера, у вигляді фільтрів або гачків.

Деякі веб-фреймворки визначають методи на кшталт preExecute() та postExecute(), але в Symfony тако немає. Гарна новина в тому, що для втручання у процес Запит -> Відповідь існує спосіб краще, з використанням компонента EventDispatcher.

Приклад валідації токена

Уявіть, що вам потрібно розробити API, в якому деякі контролери публічні, а деякі обмежені одним або декількома клієнтами. Для таких приватних функцій ви можете надати вашим клієнтам токен для їх ідентифікації.

Отже, перед тим, як виконувати дію вашого контролера, вам потрібно перевірити, чи обмежена вона. Якщо ні, то вам потрібно валідувати наданий токен.

Note

Будь ласка, відмітьте, що для простоти цього рецепту, токени будуть визначені у конфігурації, і не будуть використані ані установка, ані аутентифікація бази даних через компонент Безпека.

Фільтри "до" з подією kernel.controller

Спочатку, визначіть деяку конфігурацю токена в якості параметрів:

  • YAML
  • XML
  • PHP
1
2
3
4
5
# config/services.yaml
parameters:
    tokens:
        client1: pass1
        client2: pass2

Тегуйте контролери для перевірки

Слухач kernel.controller (він же KernelEvents::CONTROLLER) отримує сповіщення на кожний запит прямо перед тим, як виконується контролер. Так що для початку вам потрібно якимось чином вказати, чи потрібна валідація токена контролеру, що співпадає із запитом.

Простим та чистим шляхом буде створити порожній інтерфейс та змусити контролер реалізувати його:

1
2
3
4
5
6
namespace App\Controller;

interface TokenAuthenticatedController
{
    // ...
}

Контролер, що реалізує цей інтерфейс, виглядає просто так:

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace App\Controller;

use App\Controller\TokenAuthenticatedController;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class FooController extends Controller implements TokenAuthenticatedController
{
    // Дія, що потребує аутентифкації
    public function bar()
    {
        // ...
    }
}

Створення підписника подій

Далі, вам потрібно буде створити підписника подій, який міститиме логіку, яку ви хочете виконати перед вашими контролерами. Якщо ви не знайомі з підписниками подій, ви можете дізнатися про них більше у Події та слухачі подій:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// src/EventSubscriber/TokenSubscriber.php
namespace App\EventSubscriber;

use App\Controller\TokenAuthenticatedController;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;

class TokenSubscriber implements EventSubscriberInterface
{
    private $tokens;

    public function __construct($tokens)
    {
        $this->tokens = $tokens;
    }

    public function onKernelController(ControllerEvent $event)
    {
        $controller = $event->getController();

        // коли клас контролера визначає декілька методів дії, контролер
        // повертається як [$controllerInstance, 'methodName']
        if (is_array($controller)) {
            $controller = $controller[0];
        }

        if ($controller instanceof TokenAuthenticatedController) {
            $token = $event->getRequest()->query->get('token');
            if (!in_array($token, $this->tokens)) {
                throw new AccessDeniedHttpException('This action needs a valid token!');
            }
        }
    }

    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::CONTROLLER => 'onKernelController',
        ];
    }
}

Ось і все! Ваш файл services.yaml повинен бути вже встановлений для завантаження сервісів з каталогу EventSubscriber. Symfony піклується про решту. Ваш метод TokenSubscriber onKernelController() буде виконано при кожному запиті. Якщо контролер, який має бути ось-ось виконаний, реалізує токен TokenAuthenticatedController, то застосовується аутентифікація токена. Це дозволяє вам мати фільтр "до" у будь-якому бажаному вами контролері.

Tip

Якщо ваш підписник не викликається при кожному запиті, перевірте, чи завантажуєте ви сервіси з каталогу EventSubscriber і чи ввімкнена автоконфігурація . Ви також можете вручну додати тег kernel.event_subscriber.

Фільтри "після" з подією kernel.response

На додаток до "гачка", який виконується перед вашим контролером, ви також можете додати гачок, який виконуватиметься після нього. Для цього прикладу, уявіть, що ви хочете додати хеш sha1 (з сіллю, що використовує цей токен) до всіх відповідей, які пройшли аутентифікацію цього токена.

Ще одна основна подія Symfony, під назвою kernel.response (вона ж KernelEvents::RESPONSE) - сповіщується при кожному запиті, але після того, як контролер повертає обʼєкт Відповіді. Створення слухача "після" таке ж легке, як і створення класу слухача та його реєстрація в якості сервісу у цій події.

Наприклад, візьміть TokenSubscriber з попереднього прикладу і спочатку запишіть токен аутентифікації всередині атрибутів запиту. Це служитиме базовою відміткою того, що цей запит пройшов аутентиафікацію токена:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function onKernelController(FilterControllerEvent $event)
{
    // ...

    if ($controller[0] instanceof TokenAuthenticatedController) {
        $token = $event->getRequest()->query->get('token');
        if (!in_array($token, $this->tokens)) {
            throw new AccessDeniedHttpException('This action needs a valid token!');
        }

        // відмітьте запит як такий, що пройшов аутентифікацію токена
        $event->getRequest()->attributes->set('auth_token', $token);
    }
}

Тепер, сконфігуруйте підписаника так, щоб він слухав іншу подію, і додайте onKernelResponse(). Він шукатиме відмітку auth_token в обʼєкті запиту і встановлюватиме користувацький заголовок у відповіді, якщо вона буде знайдена:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// додайте нове ствердження використання нагорі у вашому файлі
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;

public function onKernelResponse(FilterResponseEvent $event)
{
    // перевірте, чи відмітив onKernelController це як токен запиту "auth'ed"
    if (!$token = $event->getRequest()->attributes->get('auth_token')) {
        return;
    }

    $response = $event->getResponse();

    // створіть хеш і встановіть його як заголовок відовіді
    $hash = sha1($response->getContent().$token);
    $response->headers->set('X-CONTENT-HASH', $hash);
}

public static function getSubscribedEvents()
{
    return array(
        KernelEvents::CONTROLLER => 'onKernelController',
        KernelEvents::RESPONSE => 'onKernelResponse',
    );
}

Ось і все! TokenSubscriber тепер буде сповіщений перед тим, як буде виконано кожний контролер (onKernelController()) та після того, як кожний контролер поверне відовідь (onKernelResponse()). Змушуючи конкретні контролери реалізовувати інтерфейс TokenAuthenticatedController, ваш слухач знає, з якими контролерами йому потрібно працювати. А зберігаючи значення у пакеті запиту "атрибути", метод onKernelResponse() знає, що потрібно додати додатковий заголовок. Повеселіться!