Привет! Сегодня мы расскажем, какие нововведения появились в контроллерах ядра за последнее время.
Для начала вспомним, что контроллеры — это часть 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.