Новое в контроллерах Bitrix Framework: фильтры и валидация

от автора

Привет! Сегодня мы расскажем, какие нововведения появились в контроллерах ядра за последнее время.

Для начала вспомним, что контроллеры — это часть MVC архитектуры, которая отвечает за обработку запроса и генерирование ответа.

Про MVC много чего написано, поэтому не будем акцентировать на этом внимание. Лучше вспомним, что контроллеры состоят из одного или нескольких действий.
А еще для действий контроллеров доступно автоматическое связывание. Мы уже рассказывали об этом тут. Если не читали, то рекомендую посмотреть. Так будет легче понимать то, о чем мы сегодня вам расскажем.

Часто возникает ситуация, когда нужно выполнить какой-либо код до или после выполнения действия контроллера. К примеру, если мы пишем действие создания задачи, то может быть уместно перед вызовом самого действия проверить, передан ли правильный заголовок Content-Type.
Такой код (выполняемый перед действием контроллера) мы разбиваем по классам и называем префильтрами. Код, который выполняется после действия контроллера — постфильтрами

Фильтры

Есть 2 способа конфигурировать (управлять префильтрами и постфильтрами) действия контроллера. Первый — переопределить метод configureActions:

<?php  use Bitrix\Main\Engine\ActionFilter\Authentication; use Bitrix\Main\Engine\Controller;  final class Entity extends Controller { public function configureActions() { return [ 'get' => [ 'prefilters' => [ new Authentication(), ], ], ]; }  public function getAction(string $id) { // ... } } 

Скоро будет доступен второй способ — конфигурация через атрибуты методов. Этот подход более современный и читаемый, но в то же время поддерживает всё, что можно сконфигурировать через configureActions:

<?php  use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Prefilters; use Bitrix\Main\Engine\ActionFilter\Authentication; use Bitrix\Main\Engine\Controller;  final class Entity extends Controller { #[Prefilters([ new Authentication() ])] public function getAction(string $id) { // ... } } 

Одновременное использование старого и нового вариантов недопустимо и будет встречено исключением Bitrix\Main\Engine\Exception\ActionConfigurationException.

Также поддерживаются дополняющие и вычитающие конструкции. Старый вариант через configureActions:

<?php  use Bitrix\Main\Engine\ActionFilter\Authentication; use Bitrix\Main\Engine\ActionFilter\Csrf; use Bitrix\Main\Engine\Controller;  final class Entity extends Controller { public function configureActions() { return [ 'get' => [ '+prefilters' => [ new Authentication(), ], '-prefilters' => [ new Csrf(), ], ], ]; }  public function getAction(string $id) { // ... } } 

Новый вариант через атрибуты:

<?php  use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\DisablePrefilters; use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\EnablePrefilters; use Bitrix\Main\Engine\ActionFilter\Authentication; use Bitrix\Main\Engine\ActionFilter\Csrf; use Bitrix\Main\Engine\Controller;  final class Entity extends Controller { #[EnablePrefilters([ new Authentication() ])] #[DisablePrefilters([ new Csrf() ])] public function getAction(string $id) { // ... } } 

И полностью аналогично для постфильтров:

<?php  use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\DisablePostfilters; use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\EnablePostfilters; use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Postfilters; use Bitrix\Main\Engine\ActionFilter\ClosureWrapper; use Bitrix\Main\Engine\ActionFilter\Cors; use Bitrix\Main\Engine\Controller;  final class Entity extends Controller { #[Postfilters([ new Cors(), ])] #[EnablePostfilters([ new Cors(), ])] #[DisablePostfilters([ new ClosureWrapper(), ])] public function getAction(string $id) { // ... } } 

Для удобства использования, чтобы не перечислять нужные пре- и постфильтры в массиве, можно использовать сразу же нужные атрибуты:

<?php  use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Authentication; use Bitrix\Main\Engine\Controller;  final class Entity extends Controller { #[Authentication()] public function getAction(string $id) { // ... } } 

Аргументы атрибутов идентичны аргументам самих экшн фильтров, за исключением последнего аргумента filterType. Он используется для дополнения или вычитания экшн фильтров.

На приведенном далее примере методы get и list будут иметь одинаковые префильтры:

<?php  use Bitrix\Main\Engine\ActionFilter\Attribute\Rule; use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\DisablePrefilters; use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\EnablePrefilters; use Bitrix\Main\Engine\ActionFilter\Authentication; use Bitrix\Main\Engine\ActionFilter\Csrf; use Bitrix\Main\Engine\ActionFilter\FilterType; use Bitrix\Main\Engine\Controller;  final class Entity extends Controller { #[EnablePrefilters([ new Authentication() ])] #[DisablePrefilters([ new Csrf() ])] public function getAction(string $id) { // ... }  #[Rule\Authentication(type: FilterType::EnablePrefilter)] #[Rule\Csrf(type: FilterType::DisablePrefilter)] public function listAction() { // ... } } 

Из коробки для всех имеющихся экшн фильтров, продублированы соответствующие атрибуты:

  • Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Authentication

  • Bitrix\Main\Engine\ActionFilter\Attribute\Rule\CloseSession

  • Bitrix\Main\Engine\ActionFilter\Attribute\Rule\ContentType

  • Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Cors

  • Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Csrf

  • Bitrix\Main\Engine\ActionFilter\Attribute\Rule\HttpMethod

  • Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Scope

  • Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Token

Как это работает под капотом

Прежде всего, все атрибуты используют внутри себя привычные фильтры. Вот пример, как выглядит атрибут Cors:

<?php  declare(strict_types=1);  namespace Bitrix\Main\Engine\ActionFilter\Attribute\Rule;  use Attribute; use Bitrix\Main\Engine\ActionFilter\Attribute\FilterAttributeInterface; use Bitrix\Main\Engine\ActionFilter\FilterType;  #[Attribute(Attribute::TARGET_METHOD)] final class Cors implements FilterAttributeInterface { public function __construct( private readonly ?string $origin = null, private readonly ?bool $credentials = null, private readonly FilterType $type = FilterType::EnablePrefilter, ) {  }  public function getFilters(): array { if ($this->type->isNegative()) { return [\Bitrix\Main\Engine\ActionFilter\Cors::class]; }  return [new \Bitrix\Main\Engine\ActionFilter\Cors($this->origin, $this->credentials)]; }  public function getType(): FilterType { return $this->type; } } 

Вначале он принимает те параметры, которые принимает сам \Bitrix\Main\Engine\ActionFilter\Cors.

Далее, в каждом атрибуте есть параметр FilterType — он и определяет тип фильтра. Вот так он выглядит:

enum FilterType: string { case Prefilter = 'prefilters'; case Postfilter = 'postfilters';  case EnablePrefilter = '+prefilters'; case DisablePrefilter = '-prefilters';  case EnablePostfilter = '+postfilters'; case DisablePostfilter = '-postfilters';  public function isNegative(): bool { return in_array($this, [self::DisablePrefilter, self::DisablePostfilter], true); } } 

Как можно заметить, ключи аналогичны методу configureActions.

Поэтому, если в нашем примере мы захотим сделать так, чтобы CloseSession был выключен, то мы сделаем так:

class Task extends \Bitrix\Main\Engine\Controller { #[\Bitrix\Main\Engine\ActionFilter\Attribute\Rule\CloseSession(type: FilterType::DisablePrefilter)] public function getAction(int $id): ?array { // ...  return $task; } } 

Аналогично для постфильтров.

Если необходимо не выключить или включать заданные постфильтры и префильтры, то можно воспользоваться атрибутами \Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Prefilters, \Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Postfilters

class Task extends \Bitrix\Main\Engine\Controller { #[Prefilters([new Csrf(), new ContentType([ContentType::JSON])])] public function getAction(int $id): ?array { // ...  return $task; } } 

Обратите внимание, что будут применены ТОЛЬКО перечисленные префильтры, а дефолтные — проигнорируются. Аналогично с постфильтрами

Создание своих атрибутов префильтров

Для этого необходимо:

  • Реализовать сам фильтр (наследник Base)

  • Написать класс атрибута, реализовав интерфейс:

interface FilterAttributeInterface { /**  * @return (Base|string)[]  */ public function getFilters(): array;  public function getType(): FilterType; } 

Пример:

#[Attribute(Attribute::TARGET_METHOD)] class MyCustomFilter implements \Bitrix\Main\Engine\ActionFilter\Attribute\FilterAttributeInterface { public function __construct( private readonly array $args, private readonly FilterType $type = FilterType::EnablePrefilter, ) {  } public function getFilters(): array { return [new MyCustomBaseFilter($this->args)] }  public function getType(): FilterType { return $this->type; } } 

Валидация

Мы также упростили валидацию параметров в контроллерах. Новой валидации мы посвятили целую статью.

Ознакомьтесь с ней, если раньше не видели — это поможет лучше понимать то, что будет дальше.

Теперь же мы улучшили связку между контроллерами и валидацией.

Напомню, что раньше для использования валидации в действиях контроллера было необходимо создавать объект, который необходимо прокинуть в специальную обертку — \Bitrix\Main\Validation\Engine\AutoWire\ValidationParameter:

class UserController extends Controller {     public function getAutoWiredParameters()     {         return [             new \Bitrix\Main\Validation\Engine\AutoWire\ValidationParameter(                 CreateUserDto::class,                 fn() => CreateUserDto::createFromRequest($this->getRequest()),             ),         ];     }          public function createAction(CreateUserDto $dto): Result     {         // create logic ...     } } 

Теперь провалидировать входные данные можно и без явного указания \Bitrix\Main\Validation\Engine\AutoWire\ValidationParameter. Достаточно указать в действии контроллера атрибуты валидации для аргументов, в том числе скалярных.

Например:

 class UserController extends \Bitrix\Main\Engine\Controller { public function createAction( #[\Bitrix\Main\Validation\Rule\PhoneOrEmail] string $login, #[\Bitrix\Main\Validation\Rule\NotEmpty] string $password, #[\Bitrix\Main\Validation\Rule\NotEmpty] string $passwordRepeat ): array { // logic here } } 

Более того, можно как и раньше передать объект, но вместо регистрации через getAutoWiredParameters достаточно будет добавить ему атрибут Validatable:

 class UserController extends \Bitrix\Main\Engine\Controller { public function createAction( #[\Bitrix\Main\Validation\Rule\Recursive\Validatable] CreateUserDto $dto) : Result { // create logic ... } } 

Этот пакет уже готовится к выпуску внутри модуля main и совсем скоро (в этом релизе) его можно будет использовать в ваших проектах на базе Bitrix Framework.


ссылка на оригинал статьи https://habr.com/ru/articles/924732/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *