@@ -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
@@ -63,7 +64,7 @@ The validator class only has one required method ``validate()``::
6364
6465 class ContainsAlphanumericValidator extends ConstraintValidator
6566 {
66- public function validate($value, Constraint $constraint)
67+ public function validate($value, Constraint $constraint): void
6768 {
6869 if (!$constraint instanceof ContainsAlphanumeric) {
6970 throw new UnexpectedTypeException($constraint, ContainsAlphanumeric::class);
@@ -97,7 +98,7 @@ The validator class only has one required method ``validate()``::
9798 }
9899 }
99100
100- Inside ``validate ``, you don't need to return a value. Instead, you add violations
101+ Inside ``validate() ``, you don't need to return a value. Instead, you add violations
101102to the validator's ``context `` property and a value will be considered valid
102103if it causes no violations. The ``buildViolation() `` method takes the error
103104message as its argument and returns an instance of
@@ -125,15 +126,15 @@ You can use custom validators like the ones provided by Symfony itself:
125126
126127 #[Assert\NotBlank]
127128 #[AcmeAssert\ContainsAlphanumeric(mode: 'loose')]
128- protected $name;
129+ protected string $name;
129130
130131 // ...
131132 }
132133
133134 .. code-block :: yaml
134135
135136 # config/validator/validation.yaml
136- App\Entity\AcmeEntity :
137+ App\Entity\User :
137138 properties :
138139 name :
139140 - NotBlank : ~
@@ -148,7 +149,7 @@ You can use custom validators like the ones provided by Symfony itself:
148149 xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
149150 xsi : schemaLocation =" http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd" >
150151
151- <class name =" App\Entity\AcmeEntity " >
152+ <class name =" App\Entity\User " >
152153 <property name =" name" >
153154 <constraint name =" NotBlank" />
154155 <constraint name =" App\Validator\ContainsAlphanumeric" >
@@ -160,18 +161,20 @@ You can use custom validators like the ones provided by Symfony itself:
160161
161162 .. code-block :: php
162163
163- // src/Entity/AcmeEntity .php
164+ // src/Entity/User .php
164165 namespace App\Entity;
165166
166167 use App\Validator\ContainsAlphanumeric;
167168 use Symfony\Component\Validator\Constraints\NotBlank;
168169 use Symfony\Component\Validator\Mapping\ClassMetadata;
169170
170- class AcmeEntity
171+ class User
171172 {
172- public $name;
173+ protected string $name = '' ;
173174
174- public static function loadValidatorMetadata(ClassMetadata $metadata)
175+ // ...
176+
177+ public static function loadValidatorMetadata(ClassMetadata $metadata): void
175178 {
176179 $metadata->addPropertyConstraint('name', new NotBlank());
177180 $metadata->addPropertyConstraint('name', new ContainsAlphanumeric(['mode' => 'loose']));
@@ -200,22 +203,62 @@ Class Constraint Validator
200203~~~~~~~~~~~~~~~~~~~~~~~~~~
201204
202205Besides validating a single property, a constraint can have an entire class
203- as its scope. You only need to add this to the ``Constraint `` class::
206+ as its scope.
207+
208+ For instance, imagine you also have a ``PaymentReceipt `` entity and you
209+ need to make sure the email of the receipt payload matches the user's
210+ email. First, create a constraint and override the ``getTargets() `` method::
211+
212+ // src/Validator/ConfirmedPaymentReceipt.php
213+ namespace App\Validator;
214+
215+ use Symfony\Component\Validator\Constraint;
204216
205- public function getTargets()
217+ /**
218+ * @Annotation
219+ */
220+ class ConfirmedPaymentReceipt extends Constraint
206221 {
207- return self::CLASS_CONSTRAINT;
222+ public string $userDoesNotMatchMessage = 'User\'s e-mail address does not match that of the receipt';
223+
224+ public function getTargets(): string
225+ {
226+ return self::CLASS_CONSTRAINT;
227+ }
208228 }
209229
210- With this, the validator's ``validate() `` method gets an object as its first argument::
230+ Now, the constraint validator will get an object as the first argument to
231+ ``validate() ``::
232+
233+ // src/Validator/ConfirmedPaymentReceiptValidator.php
234+ namespace App\Validator;
235+
236+ use Symfony\Component\Validator\Constraint;
237+ use Symfony\Component\Validator\ConstraintValidator;
238+ use Symfony\Component\Validator\Exception\UnexpectedValueException;
211239
212- class ProtocolClassValidator extends ConstraintValidator
240+ class ConfirmedPaymentReceiptValidator extends ConstraintValidator
213241 {
214- public function validate($protocol, Constraint $constraint)
242+ /**
243+ * @param PaymentReceipt $receipt
244+ */
245+ public function validate($receipt, Constraint $constraint): void
215246 {
216- if ($protocol->getFoo() != $protocol->getBar()) {
217- $this->context->buildViolation($constraint->message)
218- ->atPath('foo')
247+ if (!$receipt instanceof PaymentReceipt) {
248+ throw new UnexpectedValueException($receipt, PaymentReceipt::class);
249+ }
250+
251+ if (!$constraint instanceof ConfirmedPaymentReceipt) {
252+ throw new UnexpectedValueException($constraint, ConfirmedPaymentReceipt::class);
253+ }
254+
255+ $receiptEmail = $receipt->getPayload()['email'] ?? null;
256+ $userEmail = $receipt->getUser()->getEmail();
257+
258+ if ($userEmail !== $receiptEmail) {
259+ $this->context
260+ ->buildViolation($constraint->userDoesNotMatchMessage)
261+ ->atPath('user.email')
219262 ->addViolation();
220263 }
221264 }
@@ -227,8 +270,7 @@ With this, the validator's ``validate()`` method gets an object as its first arg
227270 associated. Use any :doc: `valid PropertyAccess syntax </components/property_access >`
228271 to define that property.
229272
230- A class constraint validator is applied to the class itself, and
231- not to the property:
273+ A class constraint validator must be applied to the class itself:
232274
233275.. configuration-block ::
234276
@@ -248,44 +290,54 @@ not to the property:
248290 .. code-block :: yaml
249291
250292 # config/validator/validation.yaml
251- App\Entity\AcmeEntity :
293+ App\Entity\PaymentReceipt :
252294 constraints :
253- - App\Validator\ProtocolClass : ~
295+ - App\Validator\ConfirmedPaymentReceipt : ~
254296
255297 .. code-block :: xml
256298
257299 <!-- config/validator/validation.xml -->
258- <class name =" App\Entity\AcmeEntity" >
259- <constraint name =" App\Validator\ProtocolClass" />
260- </class >
300+ <?xml version =" 1.0" encoding =" UTF-8" ?>
301+ <constraint-mapping xmlns =" http://symfony.com/schema/dic/constraint-mapping"
302+ xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
303+ xsi : schemaLocation =" http://symfony.com/schema/dic/constraint-mapping
304+ https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd" >
305+
306+ <class name =" App\Entity\PaymentReceipt" >
307+ <constraint name =" App\Validator\ConfirmedPaymentReceipt" />
308+ </class >
309+ </constraint-mapping >
261310
262311 .. code-block :: php
263312
264- // src/Entity/AcmeEntity .php
313+ // src/Entity/PaymentReceipt .php
265314 namespace App\Entity;
266315
267- use App\Validator\ProtocolClass ;
316+ use App\Validator\ConfirmedPaymentReceipt ;
268317 use Symfony\Component\Validator\Mapping\ClassMetadata;
269318
270- class AcmeEntity
319+ class PaymentReceipt
271320 {
272321 // ...
273322
274- public static function loadValidatorMetadata(ClassMetadata $metadata)
323+ public static function loadValidatorMetadata(ClassMetadata $metadata): void
275324 {
276- $metadata->addConstraint(new ProtocolClass ());
325+ $metadata->addConstraint(new ConfirmedPaymentReceipt ());
277326 }
278327 }
279328
280329 Testing Custom Constraints
281330--------------------------
282331
283- Use the ``ConstraintValidatorTestCase `` utility to simplify the creation of
284- unit tests for your custom constraints::
332+ Use the :class: `Symfony\\ Component\\ Validator\\ Test\\ ConstraintValidatorTestCase` `
333+ class to simplify writing unit tests for your custom constraints::
334+
335+ // tests/Validator/ContainsAlphanumericValidatorTest.php
336+ namespace App\Tests\Validator;
285337
286- // ...
287338 use App\Validator\ContainsAlphanumeric;
288339 use App\Validator\ContainsAlphanumericValidator;
340+ use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
289341
290342 class ContainsAlphanumericValidatorTest extends ConstraintValidatorTestCase
291343 {
0 commit comments