@@ -24,7 +24,7 @@ First you need to create a Constraint class and extend :class:`Symfony\\Componen
2424 */
2525 class ContainsAlphanumeric extends Constraint
2626 {
27- public $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.';
27+ public string $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.';
2828 }
2929
3030.. note ::
@@ -64,7 +64,7 @@ The validator class only has one required method ``validate()``::
6464
6565 class ContainsAlphanumericValidator extends ConstraintValidator
6666 {
67- public function validate($value, Constraint $constraint)
67+ public function validate($value, Constraint $constraint): void
6868 {
6969 if (!$constraint instanceof ContainsAlphanumeric) {
7070 throw new UnexpectedTypeException($constraint, ContainsAlphanumeric::class);
@@ -98,7 +98,7 @@ The validator class only has one required method ``validate()``::
9898 The feature to allow passing an object as the ``buildViolation() `` argument
9999 was introduced in Symfony 4.4.
100100
101- 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
102102to the validator's ``context `` property and a value will be considered valid
103103if it causes no violations. The ``buildViolation() `` method takes the error
104104message as its argument and returns an instance of
@@ -114,29 +114,29 @@ You can use custom validators like the ones provided by Symfony itself:
114114
115115 .. code-block :: php-annotations
116116
117- // src/Entity/AcmeEntity .php
117+ // src/Entity/User .php
118118 namespace App\Entity;
119119
120120 use App\Validator as AcmeAssert;
121121 use Symfony\Component\Validator\Constraints as Assert;
122122
123- class AcmeEntity
123+ class User
124124 {
125125 // ...
126126
127127 /**
128128 * @Assert\NotBlank
129129 * @AcmeAssert\ContainsAlphanumeric
130130 */
131- protected $name;
131+ protected string $name = '' ;
132132
133133 // ...
134134 }
135135
136136 .. code-block :: yaml
137137
138138 # config/validator/validation.yaml
139- App\Entity\AcmeEntity :
139+ App\Entity\User :
140140 properties :
141141 name :
142142 - NotBlank : ~
@@ -150,7 +150,7 @@ You can use custom validators like the ones provided by Symfony itself:
150150 xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
151151 xsi : schemaLocation =" http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd" >
152152
153- <class name =" App\Entity\AcmeEntity " >
153+ <class name =" App\Entity\User " >
154154 <property name =" name" >
155155 <constraint name =" NotBlank" />
156156 <constraint name =" App\Validator\ContainsAlphanumeric" />
@@ -160,18 +160,20 @@ You can use custom validators like the ones provided by Symfony itself:
160160
161161 .. code-block :: php
162162
163- // src/Entity/AcmeEntity .php
163+ // src/Entity/User .php
164164 namespace App\Entity;
165165
166166 use App\Validator\ContainsAlphanumeric;
167167 use Symfony\Component\Validator\Constraints\NotBlank;
168168 use Symfony\Component\Validator\Mapping\ClassMetadata;
169169
170- class AcmeEntity
170+ class User
171171 {
172- public $name;
172+ protected string $name = '' ;
173173
174- public static function loadValidatorMetadata(ClassMetadata $metadata)
174+ // ...
175+
176+ public static function loadValidatorMetadata(ClassMetadata $metadata): void
175177 {
176178 $metadata->addPropertyConstraint('name', new NotBlank());
177179 $metadata->addPropertyConstraint('name', new ContainsAlphanumeric());
@@ -194,22 +196,62 @@ Class Constraint Validator
194196~~~~~~~~~~~~~~~~~~~~~~~~~~
195197
196198Besides validating a single property, a constraint can have an entire class
197- as its scope. You only need to add this to the ``Constraint `` class::
199+ as its scope.
200+
201+ For instance, imagine you also have a ``PaymentReceipt `` entity and you
202+ need to make sure the email of the receipt payload matches the user's
203+ email. First, create a constraint and override the ``getTargets() `` method::
204+
205+ // src/Validator/ConfirmedPaymentReceipt.php
206+ namespace App\Validator;
198207
199- public function getTargets()
208+ use Symfony\Component\Validator\Constraint;
209+
210+ /**
211+ * @Annotation
212+ */
213+ class ConfirmedPaymentReceipt extends Constraint
200214 {
201- return self::CLASS_CONSTRAINT;
215+ public string $userDoesNotMatchMessage = 'User\'s e-mail address does not match that of the receipt';
216+
217+ public function getTargets(): string
218+ {
219+ return self::CLASS_CONSTRAINT;
220+ }
202221 }
203222
204- With this, the validator's ``validate() `` method gets an object as its first argument::
223+ Now, the constraint validator will get an object as the first argument to
224+ ``validate() ``::
225+
226+ // src/Validator/ConfirmedPaymentReceiptValidator.php
227+ namespace App\Validator;
228+
229+ use Symfony\Component\Validator\Constraint;
230+ use Symfony\Component\Validator\ConstraintValidator;
231+ use Symfony\Component\Validator\Exception\UnexpectedValueException;
205232
206- class ProtocolClassValidator extends ConstraintValidator
233+ class ConfirmedPaymentReceiptValidator extends ConstraintValidator
207234 {
208- public function validate($protocol, Constraint $constraint)
235+ /**
236+ * @param PaymentReceipt $receipt
237+ */
238+ public function validate($receipt, Constraint $constraint): void
209239 {
210- if ($protocol->getFoo() != $protocol->getBar()) {
211- $this->context->buildViolation($constraint->message)
212- ->atPath('foo')
240+ if (!$receipt instanceof PaymentReceipt) {
241+ throw new UnexpectedValueException($receipt, PaymentReceipt::class);
242+ }
243+
244+ if (!$constraint instanceof ConfirmedPaymentReceipt) {
245+ throw new UnexpectedValueException($constraint, ConfirmedPaymentReceipt::class);
246+ }
247+
248+ $receiptEmail = $receipt->getPayload()['email'] ?? null;
249+ $userEmail = $receipt->getUser()->getEmail();
250+
251+ if ($userEmail !== $receiptEmail) {
252+ $this->context
253+ ->buildViolation($constraint->userDoesNotMatchMessage)
254+ ->atPath('user.email')
213255 ->addViolation();
214256 }
215257 }
@@ -221,67 +263,76 @@ With this, the validator's ``validate()`` method gets an object as its first arg
221263 associated. Use any :doc: `valid PropertyAccess syntax </components/property_access >`
222264 to define that property.
223265
224- A class constraint validator is applied to the class itself, and
225- not to the property:
266+ A class constraint validator must be applied to the class itself:
226267
227268.. configuration-block ::
228269
229270 .. code-block :: php-annotations
230271
231- // src/Entity/AcmeEntity .php
272+ // src/Entity/PaymentReceipt .php
232273 namespace App\Entity;
233274
234- use App\Validator as AcmeAssert ;
235-
275+ use App\Validator\ConfirmedPaymentReceipt ;
276+
236277 /**
237- * @AcmeAssert\ProtocolClass
278+ * @ConfirmedPaymentReceipt
238279 */
239- class AcmeEntity
280+ class PaymentReceipt
240281 {
241282 // ...
242283 }
243284
244285 .. code-block :: yaml
245286
246287 # config/validator/validation.yaml
247- App\Entity\AcmeEntity :
288+ App\Entity\PaymentReceipt :
248289 constraints :
249- - App\Validator\ProtocolClass : ~
290+ - App\Validator\ConfirmedPaymentReceipt : ~
250291
251292 .. code-block :: xml
252293
253294 <!-- config/validator/validation.xml -->
254- <class name =" App\Entity\AcmeEntity" >
255- <constraint name =" App\Validator\ProtocolClass" />
256- </class >
295+ <?xml version =" 1.0" encoding =" UTF-8" ?>
296+ <constraint-mapping xmlns =" http://symfony.com/schema/dic/constraint-mapping"
297+ xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
298+ xsi : schemaLocation =" http://symfony.com/schema/dic/constraint-mapping
299+ https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd" >
300+
301+ <class name =" App\Entity\PaymentReceipt" >
302+ <constraint name =" App\Validator\ConfirmedPaymentReceipt" />
303+ </class >
304+ </constraint-mapping >
257305
258306 .. code-block :: php
259307
260- // src/Entity/AcmeEntity .php
308+ // src/Entity/PaymentReceipt .php
261309 namespace App\Entity;
262310
263- use App\Validator\ProtocolClass ;
311+ use App\Validator\ConfirmedPaymentReceipt ;
264312 use Symfony\Component\Validator\Mapping\ClassMetadata;
265313
266- class AcmeEntity
314+ class PaymentReceipt
267315 {
268316 // ...
269317
270- public static function loadValidatorMetadata(ClassMetadata $metadata)
318+ public static function loadValidatorMetadata(ClassMetadata $metadata): void
271319 {
272- $metadata->addConstraint(new ProtocolClass ());
320+ $metadata->addConstraint(new ConfirmedPaymentReceipt ());
273321 }
274322 }
275323
276324 Testing Custom Constraints
277325--------------------------
278326
279- Use the ``ConstraintValidatorTestCase `` utility to simplify the creation of
280- unit tests for your custom constraints::
327+ Use the :class: `Symfony\\ Component\\ Validator\\ Test\\ ConstraintValidatorTestCase` `
328+ class to simplify writing unit tests for your custom constraints::
329+
330+ // tests/Validator/ContainsAlphanumericValidatorTest.php
331+ namespace App\Tests\Validator;
281332
282- // ...
283333 use App\Validator\ContainsAlphanumeric;
284334 use App\Validator\ContainsAlphanumericValidator;
335+ use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
285336
286337 class ContainsAlphanumericValidatorTest extends ConstraintValidatorTestCase
287338 {
0 commit comments