Предисловие

Привет! Меня зовут Никита, я разработчик в компании Битрикс24. В разработке мы давно стремимся к единообразию. Это позволяет нам уменьшить количество типовых ошибок, снизить затраты на производство и повысить качество.

Валидация входных данных — это как раз один из тех механизмов, который мы привели к единообразному виду. Мы ставили перед собой задачу привести всю валидацию к одной точке входа, но при этом сохранить гибкость и оставить возможность разработчикам писать свои правила валидации.

Часто случается, что необходимо проверить сущность на «правильность», при этом не привязываясь к бизнес‑логике. К примеру, если свойство класса представляет собой id пользователя, то становится очевидным, что значение этого свойства не может быть меньше, чем 1.

Проверки вида

public function __construct(int $userId)
{
    if ($userId <= 0)
    {
        throw new \Exception();
    }
    
    $this->userId = $userId;
}

или

public function setEmail(string $email)
{
    if (!check_email($email))
    {
        throw new \Exception();
    }
    
    $this->email = $email;
}

Давно расползлись по разным модулям, увеличивая объем кода. Чтобы избежать этого, была сделана валидация, построенная на атрибутах.

Задаем нужные правила

Проще всего рассмотреть на примере. Давайте создадим класс, описывающий пользователя:

final class User
{
    private ?int $id;
    private ?string $email;
    private ?string $phone;
        
    // getters & setters ...
}

У сущности есть ряд ограничений:

  • id должен быть больше 0;

  • email должен быть валидным адресом;

  • phone должен быть валидным телефоном;

  • обязательно должен быть заполнен email или phone;

Добавим атрибуты валидации, чтобы эти требования удовлетворить:

use Bitrix\Main\Validation\Rule\AtLeastOnePropertyNotEmpty;
use Bitrix\Main\Validation\Rule\Email;
use Bitrix\Main\Validation\Rule\Phone;
use Bitrix\Main\Validation\Rule\PositiveNumber;

#[AtLeastOnePropertyNotEmpty(['email', 'phone'])]
final class User
{
    #[PositiveNumber]
    private ?int $id;
    
    #[Email]
    private ?string $email;
    
    #[Phone]
    private ?string $phone;
    
    // getters &amp; setters ...
}

Теперь мы можем осуществить валидацию через \Bitrix\Main\Validation\ValidationService, который можно достать через локатор по ключу main.validation.service.
Подход через сервис позволяет валидировать класс в том месте, где это нужно. К примеру, если объект собирается пошагово и должен быть "собран", к примеру, при сохранении его в базу данных:

use Bitrix\Main\DI\ServiceLocator;
use Bitrix\Main\Validation\ValidationService;

class UserService
{
    private ValidationService $validation;
    
    public function __construct()
    {
        $this->validation = ServiceLocator::getInstance()->get('main.validation.service');
    }
    
    public function create(?string $email, ?string $phone): Result
    {
        $user = new User();
        $user->setEmail($email);
        $user->setPhone($phone);
        
        $result = $this->validation->validate($user);
        if (!$result->isSuccess())
        {
            return $result;
        }
        
        // save logic ...
    }
}

Давайте подробнее пройдемся по коду. Главный герой здесь - \Bitrix\Main\Validation\ValidationService. Он предоставляет
1 метод - validate(), возвращающий \Bitrix\Main\Validation\ValidationResult.

Результат валидации внутри будет содержать ошибки всех сработавших валидаторов.

Результат валидации хранит в себе \Bitrix\Main\Validation\ValidationError.

ВАЖНО:

  • модификаторы доступа у свойств не учитываются в процессе проверки, валидация происходит через рефлексию

  • если атрибут отмечен как nullable и его значение не установлено, то он будет пропущен при валидации

Валидация вложенных объектов

Часто случается, что объект сложный, и в качестве свойств имеет вложенные объекты. Для того чтобы эти объекты также были отвалидированы, необходимо к такому свойству добавить атрибут \Bitrix\Main\Validation\Rule\Recursive\Validatable. Это будет служить указанием к тому, что такой объект также должен быть провалидирован при валидации объекта, который его содержит.
Объект-свойство будет провалидирован согласно всем правилам, описанным выше.

В этом случае код ошибки будет строиться исходя из названия свойства и его вложенности.

Пример:

use Bitrix\Main\Validation\Rule\Composite\Validatable;
use Bitrix\Main\Validation\Rule\NotEmpty;
use Bitrix\Main\Validation\Rule\PositiveNumber;

class Buyer
{
	#[PositiveNumber]
	public ?int $id;

	#[Validatable]
	public ?Order $order;
}

class Order
{
	#[PositiveNumber]
	public int $id;

	#[Validatable]
	public ?Payment $payment;
}

class Payment
{
	#[NotEmpty]
	public string $status;

	#[NotEmpty(errorMessage: 'Custom message error')]
	public string $systemCode;
}


// validation

/** @var \Bitrix\Main\Validation\ValidationService $validationService */
$validationService = \Bitrix\Main\DI\ServiceLocator::getInstance()->get('main.validation.service');

$buyer = new Buyer();
$buyer->id = 0;
$result1 = $validationService->validate($buyer);

// "id: Значение поля меньше допустимого"
foreach ($result1->getErrors() as $error)
{
	echo $error->getCode() . ': ' . $error->getMessage(). PHP_EOL;
}

echo PHP_EOL;

$buyer->id = 1;

$order = new Order();
$order->id = -1;
$buyer->order = $order;

$result2 = $validationService->validate($buyer);

// "order.id: Значение поля меньше допустимого"
foreach ($result2->getErrors() as $error)
{
	echo $error->getCode() . ': ' . $error->getMessage(). PHP_EOL;
}

echo PHP_EOL;

$buyer->order->id = 123;

$payment = new Payment();
$payment->status = '';
$payment->systemCode = '';

$buyer->order->payment = $payment;
$result3 = $validationService->validate($buyer);

// "order.payment.status: Значение поля не может быть пустым"
// "order.payment.systemCode: Custom message error"
foreach ($result3->getErrors() as $error)
{
	echo $error->getCode() . ': ' . $error->getMessage(). PHP_EOL;
}

Валидация в контроллерах

Контроллеры - это часть MVC архитектуры, которая отвечает за обработку запроса и генерирование ответа. Это та часть нашего фреймворка, которая практически всегда взаимодействует с пользовательскими данными. Поэтому крайне важно было внедрить в них созданный механизм валидации, что позволило разработчикам избавиться от рутины при проверке данных.

Рассмотрим пример валидации в контроллере.

Допустим, у нас есть DTO и контроллер:

use Bitrix\Main\Validation\Rule\NotEmpty;
use Bitrix\Main\Validation\Rule\PhoneOrEmail;

final class CreateUserDto
{
    public function __construct(
        #[PhoneOrEmail]
        public ?string $login,
        
        #[NotEmpty]
        public ?string $password,
        
        #[NotEmpty]
        public ?string $passwordRepeat,
    )
    {}
}

Использование этого класса в коде будет выглядеть так:

class UserController extends Controller
{
    private ValidationService $validation;
    
    protected function init()
    {
        parent::init();
        
        $this->validation = ServiceLocator::getInstance()->get('main.validation.service');
    }
    
    public function createAction(): Result
    {
        $dto = new CreateUserDto();
        $dto->login = (string)$this->getRequest()->get('login');
        $dto->password = (string)$this->getRequest()->get('password');
        $dto->passwordRepeat = (string)$this->getRequest()->get('passwordRepeat');
        
        $result = $this->validation->validate($dto);
        if (!$result->isSuccess())
        {
            $this->addErrors($result->getErrors());
            
            return false;
        }
        
        // create logic ...
    }
}

Кусок кода с инициализацией и валидацией будет повторяться от метода к методу.
Чтобы этого избежать и облегчить код, для начала создадим фабричный метод в DTO:

final class CreateUserDto
{
    public function __construct(
        #[PhoneOrEmail]
        public ?string $login = null,
        
        #[NotEmpty]
        public ?string $password = null,
        
        #[NotEmpty]
        public ?string $passwordRepeat = null,
    )
    {}
    
    public static function createFromRequest(\Bitrix\Main\HttpRequest $request): self
    {
        return new static(
            login: (string)$request->getRequest()->get('login'),
            password: (string)$request->getRequest()->get('password'),
            passwordRepeat: (string)$request->getRequest()->get('passwordRepeat'),
        );
    }
}

И воспользуемся автоварингом контроллера и специальным классом 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 ...
    }
}

В случае, если объект CreateUserDto будет не валиден, до метода createAction код не дойдёт, а контроллер вернёт ответ с ошибкой:

{
	data: null,
	errors:
	[
		{
			code: "name",
			customData: null,
			message: "Значение поля не должно быть пустым",
		},
	],
	status: "error"
}

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

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

use Bitrix\Main\Validation\Validator\EmailValidator;

$email = 'bitrix@bitrix.com';
$validator = new EmailValidator();
$result = $validator->validate($email);
if (!$result->isSuccess())
{
	// ...
}

Кастомные ошибки

Есть возможность указания своего текста ошибки, который будет возвращен после валидации.

Вот пример валидации с указанием кастомной ошибки:

use Bitrix\Main\Validation\Rule\PositiveNumber;

class User
{
	public function __construct(
		#[PositiveNumber(errorMessage: 'Invalid ID!')]
		public readonly int $id
	)
	{
	}
}

$user = new User(-150);

/** @var \Bitrix\Main\Validation\ValidationService $service */
$result = $service->validate($user);

foreach ($result->getErrors() as $error)
{
	echo $error->getMessage();
}

// output: 'Invalid ID!'

И без кастомной ошибки (используется стандартная ошибка валидатора):

use Bitrix\Main\Validation\Rule\PositiveNumber;

class User
{
	public function __construct(
		#[PositiveNumber]
		public readonly int $id
	)
	{
	}
}

$user = new User(-150);

/** @var \Bitrix\Main\Validation\ValidationService $service */
$result = $service->validate($user);

foreach ($result->getErrors() as $error)
{
	echo $error->getMessage();
}

// output: 'Значение поля меньше допустимого'

Получить сработавший валидатор

Как говорилось ранее, результат валидации хранит в себе ошибки - \Bitrix\Main\Validation\ValidationError.
Вообще говоря, это наследник нашей любимой \Bitrix\Main\Error, но с одним «но» — у нашей ошибки есть свойство $this->failedValidator. Это свойство обычно содержит упавший валидатор, так как метафизическая связь валидатора и ошибки — это 1 к 1. Мы говорим «обычно содержит», потому что в общем случае атрибут может не использовать внутри себя валидаторы.

$errors = $service->validate($dto)->getErrors();
foreach ($errors as $error)
{
    $failedValidator = $error->getFailedValidator();
    // ...
}

Список атрибутов и валидаторов в поставке

Атрибуты:

Свойства:

  • ElementsType — все элементы перечисляемого свойства должны быть заданного типа;

  • Email

  • InArray — значение свойства является одним из элементов массива (для случаев, когда по какой‑то причине не удалось использовать Enum)

  • Length

  • Max

  • Min

  • NotEmpty

  • Phone

  • PhoneOrEmail — свойство является либо телефоном, либо почтой

  • PositiveNumber

  • Range

  • RegExp

  • Url

Классы:

  • AtLeastOnePropertyNotEmpty — проверяет, что хотя бы одно свойство из заданных не пустое (названия свойств прокидываются в конструктор)

Валидаторы:

  • AtLeastOnePropertyNotEmpty — проверяет, что хотя бы одно свойство из заданных не пустое (названия свойств прокидываются в конструктор)

  • Email;

  • InArray — значение свойства является одним из элементов массива (для случаев, когда по какой‑то причине не удалось использовать Enum)

  • Length

  • Max

  • Min

  • NotEmpty

  • Phone

  • RegExp

  • Url

Создание валидаторов

Валидаторы

Если атрибуты представляют собой классы, которые могут содержать различную логику, то валидаторы содержат одну элементарную операцию.

Каждый валидатор реализует \Bitrix\Main\Validation\Validator\ValidatorInterface с методом public function validate(mixed $value): ValidationResult.

Как можно заметить из сигнатуры, валидатор просто валидирует значение. У него нет контекста, является ли это значение свойством или классом. Он даже не знает, что он привязан к атрибуту. Он просто мелкий «кубик», из которого строится «башня» валидации.

Давайте рассмотрим пример валидатора Min:

namespace Bitrix\Main\Validation\Validator;

use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Validation\ValidationError;
use Bitrix\Main\Validation\ValidationResult;
use Bitrix\Main\Validation\Validator\ValidatorInterface;

final class Min implements ValidatorInterface
{
    public function __construct(
        private readonly int $min
    )
    {
    }

    public function validate(mixed $value): ValidationResult
    {
        $result = new ValidationResult();

        if (!is_numeric($value))
        {
            $result->addError(
                new ValidationError(
                    Loc::getMessage('MAIN_VALIDATION_MIN_NOT_A_NUMBER'),
                    failedValidator: $this
                )
            );

            return $result;
        }

        if ($value < $this->min)
        {
            $result->addError(
                new ValidationError(
                    Loc::getMessage('MAIN_VALIDATION_MIN_LESS_THAN_MIN'),
                    failedValidator: $this)
            );
        }

        return $result;
    }
}

Создание атрибутов валидации

Атрибуты

Атрибуты делятся на 2 типа: атрибуты свойств (на примере выше) и атрибуты класса.
В общем случае, класс атрибута свойства должен реализовывать интерфейс \Bitrix\Main\Validation\Rule\PropertyValidationAttributeInterface и его метод public function validateProperty(mixed $propertyValue): ValidationResult;, чтобы сервис воспринял этот класс как атрибут для валидации свойства.

Тут мы и предоставляем гибкость, о которой я говорил вначале. Вам достаточно создать класс атрибута, реализовать нужный интерфейс, пометить этим атрибутом нужное свойство или класс, и он будет отвалидирован согласно тем правилам, которые вы описали в своем атрибуте.

Кастомные ошибки

Наследуясь от абстрактных классов AbstractClassValidationAttribute, AbstractPropertyValidationAttribute мы получаем возможность задать в конструкторе атрибута свойство $errorMessage. Это строка. Если они передана, то вместо ошибок валидаторов, вернётся единственная ошибка с указанным в $errorMessage текстом.

Атрибуты свойства

Давайте напишем простой атрибут для валидации свойства:

use Bitrix\Main\Validation\Rule\PropertyValidationAttributeInterface;
use Bitrix\Main\Validation\ValidationError;
use Bitrix\Main\Validation\ValidationResult;

#[Attribute(Attribute::TARGET_PROPERTY)]
class NotOne implements PropertyValidationAttributeInterface
{
    public function validateProperty(mixed $propertyValue): ValidationResult
    {
        $result = new ValidationResult();
        if ($propertyValue === 1)
        {
            $result->addError(new ValidationError('Not one'));
        }
        
        return $result;
    }
}

Всё просто - мы принимаем значение свойства, а возвращаем результат, в который складываем ValidationError.

Но часто валидация, подобно конструктору, строится из более мелких и часто встречающихся «кубиков» — что будет с нашим атрибутом NotOne, если мы, к примеру, захотим, чтобы значение свойства было обязательно больше, чем -2? Не делать же нам еще один атрибут...

Поэтому в архитектуре атрибутов есть возможность не реализовывать интерфейс PropertyValidationAttributeInterface, а отнаследоваться от абстрактного класса \Bitrix\Main\Validation\Rule\AbstractPropertyValidationAttribute, который позволяет создавать атрибут из «кубиков» — валидаторов.

Абстрактный класс берет на себя ответственность за детали реализации валидации, а с нас просит реализовать абстрактный метод abstract protected function getValidators(): array.

Чтобы стало понятнее, давайте посмотрим на реализацию атрибута Range:

Он содержит в себе 2 пазла — Min, Max. А механизм абстрактного класса просто реализуют метод validateProperty, вызывая валидаторы по очереди.

use Attribute;
use Bitrix\Main\Validation\Rule\AbstractPropertyValidationAttribute;
use Bitrix\Main\Validation\Validator\Implementation\Max;
use Bitrix\Main\Validation\Validator\Implementation\Min;

#[Attribute(Attribute::TARGET_PROPERTY)]
final class Range extends AbstractPropertyValidationAttribute
{
    public function __construct(
        private readonly int $min,
        private readonly int $max,
        protected ?string $errorMessage = null
    )
    {
    }

    protected function getValidators(): array
    {
        return [
            (new Min($this->min)),
            (new Max($this->max)),
        ];
    }
}

Атрибуты класса

И снова вернемся к атрибутам, но на этот раз к атрибутам класса. Иерархия наследования и реализации практически параллельная валидации свойств. Есть интерфейс \Bitrix\Main\Validation\Rule\ClassValidationAttributeInterface, который нужно реализовать. Конкретно - метод public function validateObject(object $object): ValidationResult, в который приходит валидируемый объект. Тут, к сожалению, нельзя установить какой-то общий сценарий валидации, как со свойствами - с классом всегда по-разному.

Также есть абстрактный класс \Bitrix\Main\Validation\Rule\AbstractClassValidationAttribute, но он содержит в себе лишь возможность определения кастомных ошибок, в которых чуть позже.

Вот пример реализации:

use Bitrix\Main\Validation\ValidationResult;
use Bitrix\Main\Validation\ValidationError;
use Bitrix\Main\Validation\Rule\AbstractClassValidationAttribute;
use ReflectionClass;

#[Attribute(Attribute::TARGET_CLASS)]
class NotOne extends AbstractClassValidationAttribute
{
    public function validateObject(object $object): ValidationResult
    {
        $result = new ValidationResult();
        $properties = (new ReflectionClass($object))->getProperties();
        
        if (count($properties) > 2)
        {
            $result->addError(new ValidationError('error...'));
        }
        
        return $result;
    }
}

Диаграмма классов

Для тех, кому интересно, как выглядит архитектура этого пакета, прилагаю диаграмму классов.

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

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


  1. Adgh
    29.10.2024 13:08

    Для 1С-Битрикс: Управление сайтом тоже будет доступно?


    1. callStatic Автор
      29.10.2024 13:08

      Да, потому что функционал находится в модуле main


  1. ddruganov
    29.10.2024 13:08

    А зачем заниматься собственным велосипедом, если можно было взять симфонийский валидатор и адаптировать?


    1. rpsv
      29.10.2024 13:08

      А чем Битрикс хуже чем Yii или Laravel? Тоже хочется велосипед :)


      1. SerafimArts
        29.10.2024 13:08

        Чем хуже конкретно данный валидатор?

        Да дофига чем:

        1. Атрибуты, которые семантически должны представлять из себя классы метадаты отвечают ещё за валидацию. Смешение логики и правил маппингов.

        2. В примерах кода не вижу вообще возможностей вынести правила валидации отдельно от DTO, например в yaml, php, json, etc. Чтобы правила можно было указывать во внешних файлах и изолировать от DTO.

        3. Не вижу зависимых правил и сложных выражений, типа When X then (Y or Z). И даже реализовать подобное невозможно, кажется, учитывая то что в правила валидации приходит значение только одного поля.

        4. Способа внедрить в конструктор в валидаторы тоже не вижу, т.к. см. п.1. Ну типа есть, например, сервис, предоставляющий анализ текста на "мат", как его всунуть в валидатор, чтоб сказать: #[DoesNotContainSwearWords(criteria: 0.2)], который уже делегируется нужному валидатору, который содержит инфу и локализации, и нужные коннекты к сервису и прочее.

        5. Контекста и прочей инфы тоже нет, т.к. см. п.1

        6. А в виде пакета как это поставить и посмотреть? Ну вот я хочу использовать у себя этот пакет отдельно в проекте, например...

        Могу ещё накидать чем это хуже)

        Ну т.е. если сравнивать с типичным кодом Битрикса, то это огромный шаг вперёд и выглядит на первый взгляд даже годно. Тут можно только поздравить, что у разработчиков наконец удалось написать код, от которого не хочется сразу вырвать себе глаза. Но это касается исключительно неймспейса Bitrix\Main\Validation\Xxx...

        А если же сравнивать с тем же Symfony, то, например у меня, сразу возникает такой же вопрос "зачем": Т.к. фактически это копипаста существующего решения, но с худшей реализацией и наличием критичных (в т.ч. архитектурных) косяков, которые не позволяют нормально использовать этот компонент. Ну помимо того, что использовать его просто невозможно ввиду отсутствия в пакагисте)))

        Отсюда, с точки зрения здравого смысла и этот вопрос: Зачем делать то, что и так существует, только архитектурно неправильно и в целом хуже? И варианта кроме как "потому что могут" тут, боюсь что нет.


        1. rpsv
          29.10.2024 13:08

          Чем хуже конкретно данный валидатор?

          Подкол был в том, что у вас возникли вопросы к Битрикс, и не возникли к Yii и Laravel, которые тоже свои валидаторы сделали, а не заиспользовали Symfony ;)

          1. Атрибуты, которые семантически должны представлять из себя классы метадаты отвечают ещё за валидацию. Смешение логики и правил маппингов.

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

          2. В примерах кода не вижу вообще возможностей вынести правила валидации отдельно от DTO, например в yaml, php, json, etc. Чтобы правила можно было указывать во внешних файлах и изолировать от DTO.

          Как мне кажется, нигде кроме symfony вы и не найдёте вынос правил валидации в yaml файл :D

          Нужно ли валидацию насколько далеко уносить от кода, который её использует? Очень сомнительно.

          3. Не вижу зависимых правил и сложных выражений, типа When X then (Y or Z). И даже реализовать подобное невозможно, кажется, учитывая то что в правила валидации приходит значение только одного поля.

          Если речь про атрибут свойства - то да, валидироваться будет только значение одного свойства.

          Если сделать атрибут класса - то он принимает весь объект и может формировать любые правила и условия. В статье проверка нескольких полей отражена в примере AtLeastOnePropertyNotEmpty

          Напишите пример как это должно работать по вашему мнению, желательно на примере валидаторов симфони, раз уж вы начали сравнивать с ним ;)

          4.Способа внедрить в конструктор в валидаторы тоже не вижу, т.к. см. п.1. Ну типа есть, например, сервис, предоставляющий анализ текста на "мат", как его всунуть в валидатор, чтоб сказать: #[DoesNotContainSwearWords(criteria: 0.2)], который уже делегируется нужному валидатору, который содержит инфу и локализации, и нужные коннекты к сервису и прочее.

          А в чем проблема? Создаете атрибут DoesNotContainSwearWords , который принимает все настройки и поставляет нужный валидатор, создаете собственно этот валидатор DoesNotContainSwearWordsValidator , который реализует необходимую вам логику.

          5.Контекста и прочей инфы тоже нет, т.к. см. п.1

          А прочей это какой? И насколько это надо для операции валидации конкретного поля?

          Опять же если сопроводите примерами из symfony, будет невероятно великолепно ;)

          6.А в виде пакета как это поставить и посмотреть? Ну вот я хочу использовать у себя этот пакет отдельно в проекте, например...

          А вы в целом работали с Битрикс? Знаете что он не через composer устанавливается? :)

          А если же сравнивать с тем же Symfony, то, например у меня, сразу возникает такой же вопрос "зачем": Т.к. фактически это копипаста существующего решения, но с худшей реализацией и наличием критичных (в т.ч. архитектурных) косяков, которые не позволяют нормально использовать этот компонент. Ну помимо того, что использовать его просто невозможно ввиду отсутствия в пакагисте)))

          Потому что Битрикс это не библиотечка, которая собирается из packagist-a . Внедрять сторонний компонент, с кучей сторонних зависимостей, в коммерческий проект, который затем неизбежно будет зависеть от этого стороннего решения - крайне плохая мысль. Тем более если речь про конкретный функционал.

          Адаптация и интеграция того самого великолепного решения, которое лучше в тыщу раз, под используемые в Битрикс классы и сущности также не задача из разряда "просто сделай composer require".

          Ну и вам уже надо определиться с вашим отношением к валидации в Битрикс: это всё-таки "копи-паста" или же это "худшая реализация" ;)

          Отсюда, с точки зрения здравого смысла и этот вопрос: Зачем делать то, что и так существует, только архитектурно неправильно и в целом хуже? И варианта кроме как "потому что могут" тут, боюсь что нет.

          Я тоже с точки зрения здравого смысла не понимаю зачем нужны еще какие-то решения и фреймворки, если есть symfony ;)


  1. Alian3785
    29.10.2024 13:08

    Это ведь комментарии да?


    1. Alian3785
      29.10.2024 13:08

      Ааа так это и есть атрибуты, ничего себе, кто бы мог подумать!


  1. gomoloff
    29.10.2024 13:08

    Чем рутина написания валидации для конкретной формы на РНР или Джаваскрипте или подключением какого нибудь "симфонийского валидатора" отличается от рутины написания классов с атрибутами и функциями?
    И как это работает на фронте?