Как создать пользовательское ограничение валидации
Дата обновления перевода 2024-07-29
Как создать пользовательское ограничение валидации
Вы можете создать пользовательское ограничение, расширив базовый класс ограничения 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
// in the base Symfony\Component\Validator\Constraint class
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
// 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
, и вам
нужно убедиться в том, что электронная почта в содержимом квитанции совпадает
с электронной почтой пользователя. Для начала, создайте ограничение и переопределите
метод 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')];
// ...
}
}