Skip to content

Commit 6392428

Browse files
committed
minor #17335 [Validator] Modernize Custom constraints article (finishes #13898) (henry2778, wouterj)
This PR was merged into the 4.4 branch. Discussion ---------- [Validator] Modernize Custom constraints article (finishes #13898) Replaces #13898 While the main addition of that PR was unit tests (which we merged from a concurrent PR), it also brought some great other things: Real & consistent examples. Let's get them in the docs :) Also, I've added the type declarations to this article (like we are doing in most examples at the moment). Commits ------- 7bd5193 [Validator] Combine #13898 with recent changes 9cbca1a [Validator] Unit Tests
2 parents 76f2c79 + 7bd5193 commit 6392428

File tree

1 file changed

+92
-41
lines changed

1 file changed

+92
-41
lines changed

validation/custom_constraint.rst

+92-41
Original file line numberDiff line numberDiff line change
@@ -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
102102
to the validator's ``context`` property and a value will be considered valid
103103
if it causes no violations. The ``buildViolation()`` method takes the error
104104
message 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

196198
Besides 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

Comments
 (0)