How to Create a Custom Validation Constraint

You can create a custom constraint by extending the base constraint class, Constraint. As an example you're going to create a basic validator that checks if a string contains only alphanumeric characters.

基本制約クラス Constraint を拡張することにより、カスタム制約を作成できます。例として、文字列に英数字のみが含まれているかどうかをチェックする基本的なバリデータを作成します。

Creating the Constraint Class

First you need to create a Constraint class and extend Constraint:

まず、Constraint クラスを作成し、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 = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.';
    // If the constraint has configuration options, define them as public properties
    public string $mode = 'strict';
}

Add #[\Attribute] to the constraint class if you want to use it as an attribute in other classes.

他のクラスで属性として使用する場合は、制約クラスに #[\Attribute] を追加します。

6.1

6.1

The #[HasNamedArguments] attribute was introduced in Symfony 6.1.

#[HasNamedArguments] 属性は Symfony 6.1 で導入されました。

You can use #[HasNamedArguments] to make some constraint options required:

#[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 = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.';
    public string $mode;

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

Creating the Validator itself

As you can see, a constraint class is fairly minimal. The actual validation is performed by another "constraint validator" class. The constraint validator class is specified by the constraint's validatedBy() method, which has this default logic:

ご覧のとおり、制約クラスはかなり最小限です。実際の検証は、別の「制約バリデータ」クラスによって実行されます。制約バリデータクラスは、制約の validatedBy() メソッドによって指定されます。このメソッドには、次のデフォルト ロジックがあります。
1
2
3
4
5
// in the base Symfony\Component\Validator\Constraint class
public function validatedBy()
{
    return static::class.'Validator';
}

In other words, if you create a custom Constraint (e.g. MyConstraint), Symfony will automatically look for another class, MyConstraintValidator when actually performing the validation.

言い換えると、カスタム Constraint (例: MyConstraint) を作成すると、Symfony は実際に検証を実行するときに MyConstraintValidator という別のクラスを自動的に探します。

The validator class only has one required method validate():

バリデータ クラスには、必要なメソッド validate() が 1 つだけあります。
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): void
    {
        if (!$constraint instanceof ContainsAlphanumeric) {
            throw new UnexpectedTypeException($constraint, ContainsAlphanumeric::class);
        }

        // custom constraints should ignore null and empty values to allow
        // other constraints (NotBlank, NotNull, etc.) to take care of that
        if (null === $value || '' === $value) {
            return;
        }

        if (!is_string($value)) {
            // throw this exception if your validator cannot handle the passed type so that it can be marked as invalid
            throw new UnexpectedValueException($value, 'string');

            // separate multiple types using pipes
            // throw new UnexpectedValueException($value, 'string|int');
        }

        // access your configuration options like this:
        if ('strict' === $constraint->mode) {
            // ...
        }

        if (!preg_match('/^[a-zA-Z0-9]+$/', $value, $matches)) {
            // the argument must be a string or an object implementing __toString()
            $this->context->buildViolation($constraint->message)
                ->setParameter('{{ string }}', $value)
                ->addViolation();
        }
    }
}

Inside validate(), you don't need to return a value. Instead, you add violations to the validator's context property and a value will be considered valid if it causes no violations. The buildViolation() method takes the error message as its argument and returns an instance of ConstraintViolationBuilderInterface. The addViolation() method call finally adds the violation to the context.

validate() 内では、値を返す必要はありません。代わりに、バリデーターのコンテキスト プロパティに違反を追加すると、違反が発生しない場合に値が有効であると見なされます。 buildViolation() メソッドはエラーメッセージを引数として受け取り、ConstraintViolationBuilderInterface のインスタンスを返します。addViolation() メソッド呼び出しは、最終的に違反をコンテキストに追加します。

Using the new Validator

You can use custom validators like the ones provided by Symfony itself:

Symfony 自体が提供するようなカスタム バリデーターを使用できます。
  • Attributes
    属性
  • YAML
    YAML
  • XML
    XML
  • PHP
    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;

    // ...
}

If your constraint contains options, then they should be public properties on the custom Constraint class you created earlier. These options can be configured like options on core Symfony constraints.

制約にオプションが含まれている場合は、前に作成したカスタム Constraint クラスのパブリック プロパティにする必要があります。これらのオプションは、コア Symfony 制約のオプションのように構成できます。

Constraint Validators with Dependencies

If you're using the default services.yaml configuration, then your validator is already registered as a service and tagged with the necessary validator.constraint_validator. This means you can inject services or configuration like any other service.

デフォルトの services.yaml 構成を使用している場合、バリデーターは既にサービスとして登録されており、必要な validator.constraint_validator でタグ付けされています。これは、他のサービスと同様に、サービスまたは構成を注入できることを意味します。

Create a Reusable Set of Constraints

In case you need to consistently apply a common set of constraints across your application, you can extend the Compound constraint.

アプリケーション全体に共通の制約セットを一貫して適用する必要がある場合は、複合制約を拡張できます。

Class Constraint Validator

Besides validating a single property, a constraint can have an entire class as its scope.

単一のプロパティを検証するだけでなく、制約はそのスコープとしてクラス全体を持つことができます。

For instance, imagine you also have a PaymentReceipt entity and you need to make sure the email of the receipt payload matches the user's email. First, create a constraint and override the getTargets() method:

たとえば、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;
    }
}

Now, the constraint validator will get an object as the first argument to validate():

これで、制約バリデーターは、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

ヒント

The atPath() method defines the property with which the validation error is associated. Use any valid PropertyAccess syntax to define that property.

atPath() メソッドは、検証エラーが関連付けられているプロパティを定義します。有効な PropertyAccess 構文を使用して、そのプロパティを定義します。

A class constraint validator must be applied to the class itself:

クラス制約バリデーターをクラス自体に適用する必要があります。
  • Attributes
    属性
  • YAML
    YAML
  • XML
    XML
  • PHP
    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
{
    // ...
}

Testing Custom Constraints

Use the ConstraintValidatorTestCase` class to simplify writing unit tests for your custom constraints:

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')];
        // ...
    }
}