@@ -28,8 +28,9 @@ First you need to create a Constraint class and extend :class:`Symfony\\Componen
2828 */
2929 class ContainsAlphanumeric extends Constraint
3030 {
31- public $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.';
32- public $mode = 'strict'; // If the constraint has configuration options, define them as public properties
31+ public string $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.';
32+ // If the constraint has configuration options, define them as public properties
33+ public string $mode = 'strict';
3334 }
3435
3536 .. code-block :: php-attributes
@@ -85,7 +86,7 @@ The validator class only has one required method ``validate()``::
8586
8687 class ContainsAlphanumericValidator extends ConstraintValidator
8788 {
88- public function validate($value, Constraint $constraint)
89+ public function validate($value, Constraint $constraint): void
8990 {
9091 if (!$constraint instanceof ContainsAlphanumeric) {
9192 throw new UnexpectedTypeException($constraint, ContainsAlphanumeric::class);
@@ -119,7 +120,7 @@ The validator class only has one required method ``validate()``::
119120 }
120121 }
121122
122- Inside ``validate ``, you don't need to return a value. Instead, you add violations
123+ Inside ``validate() ``, you don't need to return a value. Instead, you add violations
123124to the validator's ``context `` property and a value will be considered valid
124125if it causes no violations. The ``buildViolation() `` method takes the error
125126message as its argument and returns an instance of
@@ -135,21 +136,21 @@ You can use custom validators like the ones provided by Symfony itself:
135136
136137 .. code-block :: php-annotations
137138
138- // src/Entity/AcmeEntity .php
139+ // src/Entity/User .php
139140 namespace App\Entity;
140141
141142 use App\Validator as AcmeAssert;
142143 use Symfony\Component\Validator\Constraints as Assert;
143144
144- class AcmeEntity
145+ class User
145146 {
146147 // ...
147148
148149 /**
149150 * @Assert\NotBlank
150151 * @AcmeAssert\ContainsAlphanumeric(mode="loose")
151152 */
152- protected $name;
153+ protected string $name = '' ;
153154
154155 // ...
155156 }
@@ -176,7 +177,7 @@ You can use custom validators like the ones provided by Symfony itself:
176177 .. code-block :: yaml
177178
178179 # config/validator/validation.yaml
179- App\Entity\AcmeEntity :
180+ App\Entity\User :
180181 properties :
181182 name :
182183 - NotBlank : ~
@@ -191,7 +192,7 @@ You can use custom validators like the ones provided by Symfony itself:
191192 xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
192193 xsi : schemaLocation =" http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd" >
193194
194- <class name =" App\Entity\AcmeEntity " >
195+ <class name =" App\Entity\User " >
195196 <property name =" name" >
196197 <constraint name =" NotBlank" />
197198 <constraint name =" App\Validator\ContainsAlphanumeric" >
@@ -203,18 +204,20 @@ You can use custom validators like the ones provided by Symfony itself:
203204
204205 .. code-block :: php
205206
206- // src/Entity/AcmeEntity .php
207+ // src/Entity/User .php
207208 namespace App\Entity;
208209
209210 use App\Validator\ContainsAlphanumeric;
210211 use Symfony\Component\Validator\Constraints\NotBlank;
211212 use Symfony\Component\Validator\Mapping\ClassMetadata;
212213
213- class AcmeEntity
214+ class User
214215 {
215- public $name;
216+ protected string $name = '';
217+
218+ // ...
216219
217- public static function loadValidatorMetadata(ClassMetadata $metadata)
220+ public static function loadValidatorMetadata(ClassMetadata $metadata): void
218221 {
219222 $metadata->addPropertyConstraint('name', new NotBlank());
220223 $metadata->addPropertyConstraint('name', new ContainsAlphanumeric(['mode' => 'loose']));
@@ -247,22 +250,62 @@ Class Constraint Validator
247250~~~~~~~~~~~~~~~~~~~~~~~~~~
248251
249252Besides validating a single property, a constraint can have an entire class
250- as its scope. You only need to add this to the ``Constraint `` class::
253+ as its scope.
254+
255+ For instance, imagine you also have a ``PaymentReceipt `` entity and you
256+ need to make sure the email of the receipt payload matches the user's
257+ email. First, create a constraint and override the ``getTargets() `` method::
258+
259+ // src/Validator/ConfirmedPaymentReceipt.php
260+ namespace App\Validator;
261+
262+ use Symfony\Component\Validator\Constraint;
251263
252- public function getTargets()
264+ /**
265+ * @Annotation
266+ */
267+ class ConfirmedPaymentReceipt extends Constraint
253268 {
254- return self::CLASS_CONSTRAINT;
269+ public string $userDoesNotMatchMessage = 'User\'s e-mail address does not match that of the receipt';
270+
271+ public function getTargets(): string
272+ {
273+ return self::CLASS_CONSTRAINT;
274+ }
255275 }
256276
257- With this, the validator's ``validate() `` method gets an object as its first argument::
277+ Now, the constraint validator will get an object as the first argument to
278+ ``validate() ``::
279+
280+ // src/Validator/ConfirmedPaymentReceiptValidator.php
281+ namespace App\Validator;
282+
283+ use Symfony\Component\Validator\Constraint;
284+ use Symfony\Component\Validator\ConstraintValidator;
285+ use Symfony\Component\Validator\Exception\UnexpectedValueException;
258286
259- class ProtocolClassValidator extends ConstraintValidator
287+ class ConfirmedPaymentReceiptValidator extends ConstraintValidator
260288 {
261- public function validate($protocol, Constraint $constraint)
289+ /**
290+ * @param PaymentReceipt $receipt
291+ */
292+ public function validate($receipt, Constraint $constraint): void
262293 {
263- if ($protocol->getFoo() != $protocol->getBar()) {
264- $this->context->buildViolation($constraint->message)
265- ->atPath('foo')
294+ if (!$receipt instanceof PaymentReceipt) {
295+ throw new UnexpectedValueException($receipt, PaymentReceipt::class);
296+ }
297+
298+ if (!$constraint instanceof ConfirmedPaymentReceipt) {
299+ throw new UnexpectedValueException($constraint, ConfirmedPaymentReceipt::class);
300+ }
301+
302+ $receiptEmail = $receipt->getPayload()['email'] ?? null;
303+ $userEmail = $receipt->getUser()->getEmail();
304+
305+ if ($userEmail !== $receiptEmail) {
306+ $this->context
307+ ->buildViolation($constraint->userDoesNotMatchMessage)
308+ ->atPath('user.email')
266309 ->addViolation();
267310 }
268311 }
@@ -274,22 +317,21 @@ With this, the validator's ``validate()`` method gets an object as its first arg
274317 associated. Use any :doc: `valid PropertyAccess syntax </components/property_access >`
275318 to define that property.
276319
277- A class constraint validator is applied to the class itself, and
278- not to the property:
320+ A class constraint validator must be applied to the class itself:
279321
280322.. configuration-block ::
281323
282324 .. code-block :: php-annotations
283325
284- // src/Entity/AcmeEntity .php
326+ // src/Entity/PaymentReceipt .php
285327 namespace App\Entity;
286328
287- use App\Validator as AcmeAssert ;
288-
329+ use App\Validator\ConfirmedPaymentReceipt ;
330+
289331 /**
290- * @AcmeAssert\ProtocolClass
332+ * @ConfirmedPaymentReceipt
291333 */
292- class AcmeEntity
334+ class PaymentReceipt
293335 {
294336 // ...
295337 }
@@ -310,44 +352,54 @@ not to the property:
310352 .. code-block :: yaml
311353
312354 # config/validator/validation.yaml
313- App\Entity\AcmeEntity :
355+ App\Entity\PaymentReceipt :
314356 constraints :
315- - App\Validator\ProtocolClass : ~
357+ - App\Validator\ConfirmedPaymentReceipt : ~
316358
317359 .. code-block :: xml
318360
319361 <!-- config/validator/validation.xml -->
320- <class name =" App\Entity\AcmeEntity" >
321- <constraint name =" App\Validator\ProtocolClass" />
322- </class >
362+ <?xml version =" 1.0" encoding =" UTF-8" ?>
363+ <constraint-mapping xmlns =" http://symfony.com/schema/dic/constraint-mapping"
364+ xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
365+ xsi : schemaLocation =" http://symfony.com/schema/dic/constraint-mapping
366+ https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd" >
367+
368+ <class name =" App\Entity\PaymentReceipt" >
369+ <constraint name =" App\Validator\ConfirmedPaymentReceipt" />
370+ </class >
371+ </constraint-mapping >
323372
324373 .. code-block :: php
325374
326- // src/Entity/AcmeEntity .php
375+ // src/Entity/PaymentReceipt .php
327376 namespace App\Entity;
328377
329- use App\Validator\ProtocolClass ;
378+ use App\Validator\ConfirmedPaymentReceipt ;
330379 use Symfony\Component\Validator\Mapping\ClassMetadata;
331380
332- class AcmeEntity
381+ class PaymentReceipt
333382 {
334383 // ...
335384
336- public static function loadValidatorMetadata(ClassMetadata $metadata)
385+ public static function loadValidatorMetadata(ClassMetadata $metadata): void
337386 {
338- $metadata->addConstraint(new ProtocolClass ());
387+ $metadata->addConstraint(new ConfirmedPaymentReceipt ());
339388 }
340389 }
341390
342391 Testing Custom Constraints
343392--------------------------
344393
345- Use the ``ConstraintValidatorTestCase `` utility to simplify the creation of
346- unit tests for your custom constraints::
394+ Use the :class: `Symfony\\ Component\\ Validator\\ Test\\ ConstraintValidatorTestCase` `
395+ class to simplify writing unit tests for your custom constraints::
396+
397+ // tests/Validator/ContainsAlphanumericValidatorTest.php
398+ namespace App\Tests\Validator;
347399
348- // ...
349400 use App\Validator\ContainsAlphanumeric;
350401 use App\Validator\ContainsAlphanumericValidator;
402+ use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
351403
352404 class ContainsAlphanumericValidatorTest extends ConstraintValidatorTestCase
353405 {
0 commit comments