Як створити користувацьке обмеження валідації
Дата оновлення перекладу 2024-06-08
Як створити користувацьке обмеження валідації
Ви можете створити користувацьке обмеження, розширивши базовий клас обмеження Constraint. В якості прикладу ви створите простий валідатор, який перевіряє, чи містить рядок лише буквенно-цифрові знаки.
Створення класу обмеження
Спочатку вам потрібно створити клас обмеження і розширити Constraint:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// src/Validator/ContainsAlphanumeric.php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
class ContainsAlphanumeric extends Constraint
{
public string $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.';
public string $mode = 'strict';
// всі опції, доступні для конфігурування, мають бути передані конструктору
public function __construct(string $mode = null, string $message = null, array $groups = null, $payload = null)
{
parent::__construct([], $groups, $payload);
$this->mode = $mode ?? $this->mode;
$this->message = $message ?? $this->message;
}
}
Додайте #[\Attribute]
до класу обмеження, якщо ви хочете використати його як
атрибут в інших класах.
Ви можете використати #[HasNamedArguments]
, щоб зробити опції обмеження обовʼязковими:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// src/Validator/ContainsAlphanumeric.php
namespace App\Validator;
use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
class ContainsAlphanumeric extends Constraint
{
public string $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.';
#[HasNamedArguments]
public function __construct(
public string $mode,
array $groups = null,
mixed $payload = null,
) {
parent::__construct([], $groups, $payload);
}
}
Створення самого валідатора
Як ви бачите, клас обмеження достатньо мінімальний. Сама валідація виконується іншим
класом "валідатором обмеження". Клас валідатора обмеження вказується методом обмеження
validatedBy()
, який містить деяку просту логіку за замовчуванням:
1 2 3 4 5
// в базовому класі Symfony\Component\Validator\Constraint
public function validatedBy(): string
{
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 44 45 46
// 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(mixed $value, Constraint $constraint): void
{
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)) {
return;
}
// аргумент має бути рядком або об'єктом, що реалізує implementing __toString()
$this->context->buildViolation($constraint->message)
->setParameter('{{ string }}', $value)
->addViolation();
}
}
}
Всередині validate
вам не потрібно повертати значення. Замість цього ви додаєте
порушення до властивості валідатора context
і значення буде прийняте як валідне,
якщо воно не викличе ніяких порушень. Метод buildViolation()
бере повідомлення про
помилку в якості свого аргументу та повертає екземпляр
ConstraintViolationBuilderInterface.
Метод виклику addViolation()
зрештою додає порушення у контекст.
Використання нового валідатора
Використовувати користувацькі валідатори як і ті, що надаються самою Symfony:
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
. Це означає, що ви можете
впроваджувати сервіси або конфігурацію , як будь-який інший сервіс.
Валідатори обмежень з користувацькими опціями
Якщо ви хочете додати деякі опції конфігурації до вашого користувацького обмеження, спочатку визначте ці опції як публічні властивості класу обмеження:
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
// src/Validator/Foo.php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
class Foo extends Constraint
{
public $mandatoryFooOption;
public $message = 'This value is invalid';
public $optionalBarOption = false;
public function __construct(
$mandatoryFooOption,
string $message = null,
bool $optionalBarOption = null,
array $groups = null,
$payload = null,
array $options = []
) {
if (\is_array($mandatoryFooOption)) {
$options = array_merge($mandatoryFooOption, $options);
} elseif (null !== $mandatoryFooOption) {
$options['value'] = $mandatoryFooOption;
}
parent::__construct($options, $groups, $payload);
$this->message = $message ?? $this->message;
$this->optionalBarOption = $optionalBarOption ?? $this->optionalBarOption;
}
public function getDefaultOption()
{
return 'mandatoryFooOption';
}
public function getRequiredOptions()
{
return ['mandatoryFooOption'];
}
}
Потім, всередині класу валідатора ви можете отримати доступ до цих опцій безпосередньо через передачі
класу обмеження в метод validate()
:
1 2 3 4 5 6 7 8 9 10 11 12
class FooValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
// отримати доступ до будь-якої опції обмеження
if ($constraint->optionalBarOption) {
// ...
}
// ...
}
}
При використанні цього обмеження у власному додатку, ви можете передати значення користувацьких опцій так само, як ви передаєте будь-які інші опції у вбудованих обмеженнях:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// src/Entity/AcmeEntity.php
namespace App\Entity;
use App\Validator as AcmeAssert;
use Symfony\Component\Validator\Constraints as Assert;
class AcmeEntity
{
// ...
#[Assert\NotBlank]
#[AcmeAssert\Foo(
mandatoryFooOption: 'bar',
optionalBarOption: true
)]
protected $name;
// ...
}
Створіть повторно використовуваний набір обмежень
Якщо вам часто необхідно застосовувати загальний набір обмежень в різних місцях по всьому вашому додатку, ви можете розширити обмеження 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 = 'User\'s e-mail address does not match that of the receipt';
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,
щоб визначити цю властивість.
Валідатор класу обмеження застосовується до самого класу, а не його властивості:
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 40
// tests/Validator/ContainsAlphanumericValidatorTest.php
namespace App\Tests\Validator;
use App\Validator\ContainsAlphanumeric;
use App\Validator\ContainsAlphanumericValidator;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
class ContainsAlphanumericValidatorTest extends ConstraintValidatorTestCase
{
protected function createValidator(): ConstraintValidatorInterface
{
return new ContainsAlphanumericValidator();
}
public function testNullIsValid(): void
{
$this->validator->validate(null, new ContainsAlphanumeric());
$this->assertNoViolation();
}
/**
* @dataProvider provideInvalidConstraints
*/
public function testTrueIsInvalid(ContainsAlphanumeric $constraint): void
{
$this->validator->validate('...', $constraint);
$this->buildViolation('myMessage')
->setParameter('{{ string }}', '...')
->assertRaised();
}
public function provideInvalidConstraints(): \Generator
{
yield [new ContainsAlphanumeric(message: 'myMessage')];
// ...
}
}