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

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

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


  1. vanxant
    07.07.2025 11:25

    Правильной дорогой идёте, товарищи, жаль что поздновато


  1. freezemage0
    07.07.2025 11:25

    Это, конечно, всё очень интересно, но когда вы уберёте final с фабрик стандартных CRM сущностей? Когда вы исправите массовое помещение элементов смарт-процессов в поисковый индекс? Когда действие, производящее обратно-совместимое событие, перестанет собирать по-новой массив на основе экземпляра Item для каждого из обработчиков? Когда конвертеры TaskToTemplate и TemplateToTask перестанут делать вещи, обратные их наименованию? Когда в валидаторе имени модуля для b_disk_storage будет исправлено регулярное выражение, не позволяющее разработчикам добавлять собственные дисковые хранилища? Когда в сборщике доступных в БП функций появится событие, отмеченное как // TODO: send Event? Когда в шаблоне компонента bitrix:ui.tile.list будет исправлено наименование параметра bgColor, не позволяющее изменять фон ячейки? Когда создание пользовательского поля типа "дата-время" перестанет ломать карточку рабочих групп? Когда модуль pull перестанет делать запросы в цикле для того, чтобы узнать список всех существующих приватных каналов получателей пулл события? Когда в интерфейсах "ролевой модели" модуля main исчезнут конструкторы, лишающие реализацию гибкости? Когда появится возможность сохранять .settings.php для конкретных модулей? Когда появится возможность копировать объект \Bitrix\Main\ORM\Query\Query без потери данных от оригинального объекта? Когда появится возможность переиспользования Query в целом? Когда в команду orm:annotate будет добавлена возможность присоединять php-doc аннотации к дата-менеджерам, а не генерировать один огромный orm meta файл? Когда появится адекватная сборка мусора в фабриках CRM сущностей? Когда коробочная версия Битрикс24 начнёт поддерживать мультиязычность? Когда модуль задач обретёт единое API, которое не сводится к вызовам CTasks?

    Ближайшее, что смог вспомнить.

    Может быть, стоит собрать фидбэк разработчиков и поправить известные ошибки перед тем, как добавлять узконаправленную функциональность?..


  1. SerafimArts
    07.07.2025 11:25

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

    Вы же даже ссылку на вики привели, карл! Контроллер отвечает за получение событий (да пусть даже тех же Реквестов, сделаем скидку на веб-специфику) юзера и обновление моделей. Прям почти цитата из вашей ссылки.

    А за обработку запроса (события) и генерацию ответа (представления) отвечает MVP

    В остальном молодцы, сделали почти как в Symfony/Yii 15-ти летней давности (в ларке миддлвари), за исключением того, что вместо "событий" (если говорить про Symfony) используются "фильтры". Имхо, это намного лучше именование, чем "события" в симфе, т.к. последние иммутабельными должны быть. Тут грамотно.

    Далее, наличие createFromRequest метода - это уже скорее не DTO, а ValueObject. Думаю не особо критично, но нейминг мне кажется не совсем корректный. Или я ошибаюсь? Всё же это статический конструктор... Однако всё же фабричный, а DTO не должно содержать никакой логики вообще.

    P.S. В целом, то что битрикс превращается во что-то более-менее терпимое -- это безусловно плюс, однако как всегда - остаётся ощущение, что с точки зрения архитектуры/проектирования опять недоделали и это в перспективе опять вызовет проблемы:

    1. У вас есть атрибуты в которых есть объекты. Если "фильтр" -- это полноценный сервис (например CSRF), то ему требуется ссылка на сессию, энкриптер и прочие инфраструктурные сервисы. Однако вы по-определению таким способом декларации (внутри метод getFilters) запрещаете использовать DI, что требует декларации подобных штук как синглтонов (?), что опять превращает всё в набор из глобального стейта. В корректной реализации getFilters должен возвращать или название сервиса из контейнера, или вызывать какой-то метод, передавая туда настройки из атрибута.

    2. Вначале статьи вы ссылаетесь на статью, где подробно описано как можно внедрять параметры из запроса. Однако там хардкод на имя класса, но ни слова о внедрении на основе имени переменной, на основе интерфейса, на основе атрибута и прочих. При этом это всё прибито гвоздями к контроллеру. Что делать в том случае, если логку (класс) для внедрения надо расшарить на несколько контроллеров? А что, если там более сложная логика, например внедрение соединения к БД на основе объекта запроса (WeakMap<Request, ConnectionInterface>, коннекшн пул и все дела)? Я вообще не вижу там никакой возможности получения сервисов (DI) и информации о текущем состоянии (запросе).

    В общем, шаги верные, вы молодцы. Однако архитектурно новый код ограничивает и плохо спроектирован. Выглядит как ранняя альфа, которая требует переделывания/доработок с поломкой обратной совместимости.