@@ -26,8 +26,9 @@ First you need to create a Constraint class and extend :class:`Symfony\\Componen
2626 #[\Attribute]
2727 class ContainsAlphanumeric extends Constraint
2828 {
29- public $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.';
30- public $mode = 'strict'; // If the constraint has configuration options, define them as public properties
29+ public string $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.';
30+ // If the constraint has configuration options, define them as public properties
31+ public string $mode = 'strict';
3132 }
3233
3334 Add ``@Annotation `` or ``#[\Attribute] `` to the constraint class if you want to
@@ -116,7 +117,7 @@ The validator class only has one required method ``validate()``::
116117
117118 class ContainsAlphanumericValidator extends ConstraintValidator
118119 {
119- public function validate($value, Constraint $constraint)
120+ public function validate($value, Constraint $constraint): void
120121 {
121122 if (!$constraint instanceof ContainsAlphanumeric) {
122123 throw new UnexpectedTypeException($constraint, ContainsAlphanumeric::class);
@@ -150,7 +151,7 @@ The validator class only has one required method ``validate()``::
150151 }
151152 }
152153
153- Inside ``validate ``, you don't need to return a value. Instead, you add violations
154+ Inside ``validate() ``, you don't need to return a value. Instead, you add violations
154155to the validator's ``context `` property and a value will be considered valid
155156if it causes no violations. The ``buildViolation() `` method takes the error
156157message as its argument and returns an instance of
@@ -178,15 +179,15 @@ You can use custom validators like the ones provided by Symfony itself:
178179
179180 #[Assert\NotBlank]
180181 #[AcmeAssert\ContainsAlphanumeric(mode: 'loose')]
181- protected $name;
182+ protected string $name;
182183
183184 // ...
184185 }
185186
186187 .. code-block :: yaml
187188
188189 # config/validator/validation.yaml
189- App\Entity\AcmeEntity :
190+ App\Entity\User :
190191 properties :
191192 name :
192193 - NotBlank : ~
@@ -201,7 +202,7 @@ You can use custom validators like the ones provided by Symfony itself:
201202 xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
202203 xsi : schemaLocation =" http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd" >
203204
204- <class name =" App\Entity\AcmeEntity " >
205+ <class name =" App\Entity\User " >
205206 <property name =" name" >
206207 <constraint name =" NotBlank" />
207208 <constraint name =" App\Validator\ContainsAlphanumeric" >
@@ -213,18 +214,20 @@ You can use custom validators like the ones provided by Symfony itself:
213214
214215 .. code-block :: php
215216
216- // src/Entity/AcmeEntity .php
217+ // src/Entity/User .php
217218 namespace App\Entity;
218219
219220 use App\Validator\ContainsAlphanumeric;
220221 use Symfony\Component\Validator\Constraints\NotBlank;
221222 use Symfony\Component\Validator\Mapping\ClassMetadata;
222223
223- class AcmeEntity
224+ class User
224225 {
225- public $name;
226+ protected string $name = '';
227+
228+ // ...
226229
227- public static function loadValidatorMetadata(ClassMetadata $metadata)
230+ public static function loadValidatorMetadata(ClassMetadata $metadata): void
228231 {
229232 $metadata->addPropertyConstraint('name', new NotBlank());
230233 $metadata->addPropertyConstraint('name', new ContainsAlphanumeric(['mode' => 'loose']));
@@ -253,22 +256,62 @@ Class Constraint Validator
253256~~~~~~~~~~~~~~~~~~~~~~~~~~
254257
255258Besides validating a single property, a constraint can have an entire class
256- as its scope. You only need to add this to the ``Constraint `` class::
259+ as its scope.
260+
261+ For instance, imagine you also have a ``PaymentReceipt `` entity and you
262+ need to make sure the email of the receipt payload matches the user's
263+ email. First, create a constraint and override the ``getTargets() `` method::
264+
265+ // src/Validator/ConfirmedPaymentReceipt.php
266+ namespace App\Validator;
267+
268+ use Symfony\Component\Validator\Constraint;
257269
258- public function getTargets()
270+ /**
271+ * @Annotation
272+ */
273+ class ConfirmedPaymentReceipt extends Constraint
259274 {
260- return self::CLASS_CONSTRAINT;
275+ public string $userDoesNotMatchMessage = 'User\'s e-mail address does not match that of the receipt';
276+
277+ public function getTargets(): string
278+ {
279+ return self::CLASS_CONSTRAINT;
280+ }
261281 }
262282
263- With this, the validator's ``validate() `` method gets an object as its first argument::
283+ Now, the constraint validator will get an object as the first argument to
284+ ``validate() ``::
285+
286+ // src/Validator/ConfirmedPaymentReceiptValidator.php
287+ namespace App\Validator;
288+
289+ use Symfony\Component\Validator\Constraint;
290+ use Symfony\Component\Validator\ConstraintValidator;
291+ use Symfony\Component\Validator\Exception\UnexpectedValueException;
264292
265- class ProtocolClassValidator extends ConstraintValidator
293+ class ConfirmedPaymentReceiptValidator extends ConstraintValidator
266294 {
267- public function validate($protocol, Constraint $constraint)
295+ /**
296+ * @param PaymentReceipt $receipt
297+ */
298+ public function validate($receipt, Constraint $constraint): void
268299 {
269- if ($protocol->getFoo() != $protocol->getBar()) {
270- $this->context->buildViolation($constraint->message)
271- ->atPath('foo')
300+ if (!$receipt instanceof PaymentReceipt) {
301+ throw new UnexpectedValueException($receipt, PaymentReceipt::class);
302+ }
303+
304+ if (!$constraint instanceof ConfirmedPaymentReceipt) {
305+ throw new UnexpectedValueException($constraint, ConfirmedPaymentReceipt::class);
306+ }
307+
308+ $receiptEmail = $receipt->getPayload()['email'] ?? null;
309+ $userEmail = $receipt->getUser()->getEmail();
310+
311+ if ($userEmail !== $receiptEmail) {
312+ $this->context
313+ ->buildViolation($constraint->userDoesNotMatchMessage)
314+ ->atPath('user.email')
272315 ->addViolation();
273316 }
274317 }
@@ -280,8 +323,7 @@ With this, the validator's ``validate()`` method gets an object as its first arg
280323 associated. Use any :doc: `valid PropertyAccess syntax </components/property_access >`
281324 to define that property.
282325
283- A class constraint validator is applied to the class itself, and
284- not to the property:
326+ A class constraint validator must be applied to the class itself:
285327
286328.. configuration-block ::
287329
@@ -301,44 +343,54 @@ not to the property:
301343 .. code-block :: yaml
302344
303345 # config/validator/validation.yaml
304- App\Entity\AcmeEntity :
346+ App\Entity\PaymentReceipt :
305347 constraints :
306- - App\Validator\ProtocolClass : ~
348+ - App\Validator\ConfirmedPaymentReceipt : ~
307349
308350 .. code-block :: xml
309351
310352 <!-- config/validator/validation.xml -->
311- <class name =" App\Entity\AcmeEntity" >
312- <constraint name =" App\Validator\ProtocolClass" />
313- </class >
353+ <?xml version =" 1.0" encoding =" UTF-8" ?>
354+ <constraint-mapping xmlns =" http://symfony.com/schema/dic/constraint-mapping"
355+ xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
356+ xsi : schemaLocation =" http://symfony.com/schema/dic/constraint-mapping
357+ https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd" >
358+
359+ <class name =" App\Entity\PaymentReceipt" >
360+ <constraint name =" App\Validator\ConfirmedPaymentReceipt" />
361+ </class >
362+ </constraint-mapping >
314363
315364 .. code-block :: php
316365
317- // src/Entity/AcmeEntity .php
366+ // src/Entity/PaymentReceipt .php
318367 namespace App\Entity;
319368
320- use App\Validator\ProtocolClass ;
369+ use App\Validator\ConfirmedPaymentReceipt ;
321370 use Symfony\Component\Validator\Mapping\ClassMetadata;
322371
323- class AcmeEntity
372+ class PaymentReceipt
324373 {
325374 // ...
326375
327- public static function loadValidatorMetadata(ClassMetadata $metadata)
376+ public static function loadValidatorMetadata(ClassMetadata $metadata): void
328377 {
329- $metadata->addConstraint(new ProtocolClass ());
378+ $metadata->addConstraint(new ConfirmedPaymentReceipt ());
330379 }
331380 }
332381
333382 Testing Custom Constraints
334383--------------------------
335384
336- Use the ``ConstraintValidatorTestCase `` utility to simplify the creation of
337- unit tests for your custom constraints::
385+ Use the :class: `Symfony\\ Component\\ Validator\\ Test\\ ConstraintValidatorTestCase` `
386+ class to simplify writing unit tests for your custom constraints::
387+
388+ // tests/Validator/ContainsAlphanumericValidatorTest.php
389+ namespace App\Tests\Validator;
338390
339- // ...
340391 use App\Validator\ContainsAlphanumeric;
341392 use App\Validator\ContainsAlphanumericValidator;
393+ use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
342394
343395 class ContainsAlphanumericValidatorTest extends ConstraintValidatorTestCase
344396 {
0 commit comments