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

Для начала вспомним, что контроллеры — это часть 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.

Комментарии (0)