Как применять группы валидации последовательно

Дата обновления перевода 2024-07-29

Как применять группы валидации последовательно

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

Например, представьте, что у вас есть класс User и вы хотите валидировать то, отличются ли имя ползователя и пароль только в случае, если вся остальная валидация пройдёт успешно (чтобы избежать множественных сообщений об ошибке).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/Entity/User.php
namespace App\Entity;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;

#[Assert\GroupSequence(['User', 'Strict'])]
class User implements UserInterface
{
    #[Assert\NotBlank]
    private string $username;

    #[Assert\NotBlank]
    private string $password;

    #[Assert\IsTrue(
        message: 'Пароль не может совпадать с вашим именем пользователя',
        groups: ['Strict'],
    )]
    public function isPasswordSafe(): bool
    {
        return ($this->username !== $this->password);
    }
}

В этом примере, вначале будут валидированы все ограничения в группе User (что то же самое, что и группа Default). Только если все ограничения в этой группе будут валидны, будут валидирована вторая группа - Strict.

Caution

Как вы уже видели в Как применить только подмножество всех ваших ограничений валидации (группы валидации), группа Default и группа, содержащая имя класса (например, User) были идентичны. Однако, при использовании групповой последовательности, они больше не будут идентичными. Группа Default теперь будет ссылаться на групповую последовательность вместо всех ограничений, которые не принадлежат ни одной группе.

Это означает, что вам нужно использовать группу {ClassName} (например, User), при указывании групповой последовательности. При использовании
Default, вы получите бесконечную рекурсию (так как группа Default ссылается на групповую последовательность, содержащую группу Default, которая ссылается на ту же групповую последовательность...).

Caution

Вызов validate() с группой в последовательности (Strict - в предыдущем примере) приведет к валидации только с этой группой, а не со всеми группами в последовательности. Это так, потому что последовательность теперь ссылается на групповую валидацию Default.

Вы также можете определить последовательность группы в опции формы validation_groups:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Form/MyType.php
namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\GroupSequence;
// ...

class MyType extends AbstractType
{
    // ...
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'validation_groups' => new GroupSequence(['First', 'Second']),
        ]);
    }
}

Поставщики групповой последовательности

Представьте сущность User, которая может быть нормальным или премиум пользователем. Если это премиум пользователь, необходимо добавить некоторые дополнительные ограничения к сущности пользователя (например, информацию о кредитной карте). Чтобы динамически определить, какие группы стоит активировать, вы можете создать поставщика групповой последовательности. Для начала, создайте сущеность и новую группу ограничений под названием Premium:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Entity/User.php
namespace App\Entity;

use Symfony\Component\Validator\Constraints as Assert;

class User
{
    #[Assert\NotBlank]
    private string $name;

    #[Assert\CardScheme(
        schemes: [Assert\CardScheme::VISA],
        groups: ['Premium'],
    )]
    private string $creditCard;

    // ...
}

Теперь, измените класс User, чтобы реализовать GroupSequenceProviderInterface и добавьте метод getGroupSequence(), который должен вернуть массив групп для использования:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Entity/User.php
namespace App\Entity;

// ...
use Symfony\Component\Validator\GroupSequenceProviderInterface;

class User implements GroupSequenceProviderInterface
{
    // ...

    public function getGroupSequence(): array|GroupSequence
    {
        // при возвращении простого массива, если в любой из групп есть нарушение,
        // остальные группы не валидируются. Например, если 'User' неудачна, то
        // 'Premium' и 'Api' не валидируются:
        return ['User', 'Premium', 'Api'];

        // при возвращении встроенного масива, все группы, включенные в каждый массив,
        // валидируются. Например, если 'User' неудачна, 'Premium' также валидируется
        // (и вы получите и ее нарушения), но 'Api' не будет валидирована:
        return [['User', 'Premium'], 'Api'];
    }
}

Наконец, вам нужно уведомить компонент валидатор (Validator) о том, что ваш класс User предоставляет последовательность групп для валидации:

1
2
3
4
5
6
7
8
9
10
// src/Entity/User.php
namespace App\Entity;

// ...

#[Assert\GroupSequenceProvider]
class User implements GroupSequenceProviderInterface
{
    // ...
}

Продвинутый поставщик группы валидации

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

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

Вот как этого можно добиться:

  1. Определите отдельный класс поставщика групп: создайте класс, который реализует GroupProviderInterface и обрабатывает логику динамической последовательности групп;
  2. Сконфигурируйте пользователя с поставщиком: используйте опцию provider внутри атрибута
    GroupSequenceProvider, чтобы связать сущность с классом поставщика;
  3. Автомонтирование или ручное тегирование: если автомонтирование включено, ваш пользовательский поставщик будет связан автоматически. В противном случае вы должны тегировать свой сервис вручную с помощью тега validator.group_provider.
1
2
3
4
5
6
7
8
9
10
11
// src/Entity/User.php
namespace App\Entity;

// ...
use App\Validator\UserGroupProvider;

#[Assert\GroupSequenceProvider(provider: UserGroupProvider::class)]
class User
{
    // ...
}

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

Как последовательно применять ограничения в одном свойстве

Иногда вам может захотеться применять ограничения последовательно в одном свойстве. Оганичение Sequentially может решить это за вас более прямым путем, чем использование GroupSequence.