Як створити користувацьке обмеження валідації

Дата оновлення перекладу 2022-12-23

Як створити користувацьке обмеження валідації

Ви можете створити користувацьке обмеження, розширивши базовий клас обмеження Constraint. В якості прикладу ви створите простий валідатор, який перевіряє, чи містить рядок лише буквенно-цифрові знаки.

Створення класу обмеження

  • Attributes
1
2
3
4
5
6
7
8
9
10
11
12
// src/Validator/ContainsAlphanumeric.php
namespace App\Validator;

use Symfony\Component\Validator\Constraint;

#[\Attribute]
class ContainsAlphanumeric extends Constraint
{
    public string $message = 'Рядок "{{ string }}" містить незаконний символ: він може містити лише літери або цифри.';
    // Якщо обмеження має опції конфігурації, визначіть їх як публічні властивості
    public string $mode = 'strict';
}

Додайте #[\Attribute] до класу обмеження, якщо ви хочете використати його як атрибут в інших класах.

6.1

Атрибут #[HasNamedArguments] було представлено в Symfony 6.1.

Ви можете використати #[HasNamedArguments], щоб зробити опції обмеження обовʼязковими:

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

use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;

#[\Attribute]
class ContainsAlphanumeric extends Constraint
{
    public $message = 'Рядок "{{ string }}" містить незаконний символ: він може містити лише літери або цифри.';
    public string $mode;

    #[HasNamedArguments]
    public function __construct(string $mode, array $groups = null, mixed $payload = null)
    {
        parent::__construct([], $groups, $payload);
        $this->mode = $mode;
    }
}

Створення самого валідатора

Як ви бачите, клас обмеження достатньо мінімальний. Сама валідація виконується іншим класом "валідатором обмеження". Клас валідатора обмеження вказується методом обмеження validatedBy(), який містить деяку просту логіку за замовчуванням:

1
2
3
4
5
// в базовому класі Symfony\Component\Validator\Constraint
public function validatedBy()
{
    return static::class.'Validator';
}

Іншими словами, якщо ви створите користувацьке Constraint (наприклад, MyConstraint), Symfony автоматично шукатиме інший клас, MyConstraintValidator, при проведенні самої валідації.

Клас валідатора також простий і має лише один обов'язковий метод validate():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// src/Validator/ContainsAlphanumericValidator.php
namespace App\Validator;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;

class ContainsAlphanumericValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint)
    {
        if (!$constraint instanceof ContainsAlphanumeric) {
            throw new UnexpectedTypeException($constraint, ContainsAlphanumeric::class);
        }

        // користувацькі обмеження мають ігнорувати пусті значення та null, щоб
        // дозволити іншим обмеженням (NotBlank, NotNull, та ін.) попіклуватися про це
        if (null === $value || '' === $value) {
            return;
        }

        if (!is_string($value)) {
            // викличте це виключення, якщо ваш валідатор не може обробити переданий тип, щоб він міг бути відмічений як невалідний
            throw new UnexpectedValueException($value, 'string');

            // розділіть багато типів, використавши вертикальні риски
            // викличте нове UnexpectedValueException($value, 'string|int');
        }

        // отримайте доступ до ваших опцій конфігурації таким чином:
        if ('strict' === $constraint->mode) {
            // ...
        }

        if (!preg_match('/^[a-zA-Z0-9]+$/', $value, $matches)) {
            // аргумент має бути рядком або об'єктом, що реалізує implementing __toString()
            $this->context->buildViolation($constraint->message)
                ->setParameter('{{ string }}', $value)
                ->addViolation();
        }
    }
}

Всередині validate вам не потрібно повертати значення. Замість цього ви додаєте порушення до властивості валідатора context і значення буде прийняте як валідне, якщо воно не викличе ніяких порушень. Метод buildViolation() бере повідомлення про помилку в якості свого аргументу та повертає екземпляр ConstraintViolationBuilderInterface. Метод виклику addViolation() зрештою додає порушення у контекст.

Використання нового валідатора

Використовувати користувацькі валідатори як і ті, що надаються самою Symfony:

  • Attributes
  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/Entity/AcmeEntity.php
namespace App\Entity;

use App\Validator as AcmeAssert;
use Symfony\Component\Validator\Constraints as Assert;

class AcmeEntity
{
    // ...

    #[Assert\NotBlank]
    #[AcmeAssert\ContainsAlphanumeric(mode: 'loose')]
    protected string $name;

    // ...
}

Якщо ваше обмеження містить опції, то вони мають бути публічними властивостями користувацького класу обмеження, який ви створили раніше. Ці опції можуть бути сконфігуровані як опції базових обмежень Symfony.

Валідатори обмежень з залежностями

Якщо ви використовуєте конфігурацію services.yml за замовчуванням , ваш валідатор вже зареєстрований в якості сервісу та тегований необхідним validator.constraint_validator. Це означає, що ви можете впроваджувати сервіси або конфігурацію , як будь-який інший сервіс.

Створіть повторно використовуваний набір обмежень

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

Валідатор класу обмежень

Окрім валідації однієї властивості, обмеження може мати цілий клас в якості свого поля діяльності.

Наприклад, уявіть, що у вас також є сутність PaymentReceipt і вам треба гарантувати, що email корисного навантаження чеку співпадає з email користувача. Спочатку, створіть обмеження та перевизначіть метод getTargets():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Validator/ConfirmedPaymentReceipt.php
namespace App\Validator;

use Symfony\Component\Validator\Constraint;

#[\Attribute]
class ConfirmedPaymentReceipt extends Constraint
{
    public string $userDoesNotMatchMessage = 'Е-mail користувача не співпадає з тим, що у чеку';

    public function getTargets(): string
    {
        return self::CLASS_CONSTRAINT;
    }
}

Тепер, валідатор обмеження отримає обʼєкт як перший аргумент validate():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/Validator/ConfirmedPaymentReceiptValidator.php
namespace App\Validator;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedValueException;

class ConfirmedPaymentReceiptValidator extends ConstraintValidator
{
    /**
     * @param PaymentReceipt $receipt
     */
    public function validate($receipt, Constraint $constraint): void
    {
        if (!$receipt instanceof PaymentReceipt) {
            throw new UnexpectedValueException($receipt, PaymentReceipt::class);
        }

        if (!$constraint instanceof ConfirmedPaymentReceipt) {
            throw new UnexpectedValueException($constraint, ConfirmedPaymentReceipt::class);
        }

        $receiptEmail = $receipt->getPayload()['email'] ?? null;
        $userEmail = $receipt->getUser()->getEmail();

        if ($userEmail !== $receiptEmail) {
            $this->context
                ->buildViolation($constraint->userDoesNotMatchMessage)
                ->atPath('user.email')
                ->addViolation();
        }
    }
}

Tip

Метод atPath() визначає властивість, з якою асоціюється помилка валідації. Використайте будь-який валідний синтаксис PropertyAccess, щоб визначити цю властивість.

Валідатор класу обмеження застосовується до самого класу, а не його властивості:

  • Attributes
  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
// src/Entity/AcmeEntity.php
namespace App\Entity;

use App\Validator as AcmeAssert;

#[AcmeAssert\ProtocolClass]
class AcmeEntity
{
    // ...
}

Тестування користувацьких обмежень

Використайте клас ConstraintValidatorTestCase`, щоб спросити написання модульних тестів для ваших користувацьких обмежень:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// tests/Validator/ContainsAlphanumericValidatorTest.php
namespace App\Tests\Validator;

use App\Validator\ContainsAlphanumeric;
use App\Validator\ContainsAlphanumericValidator;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;

class ContainsAlphanumericValidatorTest extends ConstraintValidatorTestCase
{
    protected function createValidator()
    {
        return new ContainsAlphanumericValidator();
    }

    public function testNullIsValid()
    {
        $this->validator->validate(null, new ContainsAlphanumeric());

        $this->assertNoViolation();
    }

    /**
     * @dataProvider provideInvalidConstraints
     */
    public function testTrueIsInvalid(ContainsAlphanumeric $constraint)
    {
        $this->validator->validate('...', $constraint);

        $this->buildViolation('myMessage')
            ->setParameter('{{ string }}', '...')
            ->assertRaised();
    }

    public function provideInvalidConstraints(): iterable
    {
        yield [new ContainsAlphanumeric(message: 'myMessage')];
        // ...
    }
}