@@ -194,22 +194,112 @@ Class Constraint Validator
194
194
~~~~~~~~~~~~~~~~~~~~~~~~~~
195
195
196
196
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 ::
197
+ as its scope. Consider the following classes, that describe the receipt of some payment ::
198
198
199
- public function getTargets()
199
+ // src/AppBundle/Model/PaymentReceipt.php
200
+ class PaymentReceipt
200
201
{
201
- return self::CLASS_CONSTRAINT;
202
+ /**
203
+ * @var User
204
+ */
205
+ private $user;
206
+
207
+ /**
208
+ * @var array
209
+ */
210
+ private $payload;
211
+
212
+ public function __construct(User $user, array $payload)
213
+ {
214
+ $this->user = $user;
215
+ $this->payload = $payload;
216
+ }
217
+
218
+ public function getUser(): User
219
+ {
220
+ return $this->user;
221
+ }
222
+
223
+ public function getPayload(): array
224
+ {
225
+ return $this->payload;
226
+ }
227
+ }
228
+
229
+ // src/AppBundle/Model/User.php
230
+
231
+ class User
232
+ {
233
+ /**
234
+ * @var string
235
+ */
236
+ private $email;
237
+
238
+ public function __construct($email)
239
+ {
240
+ $this->email = $email;
241
+ }
242
+
243
+ public function getEmail(): string
244
+ {
245
+ return $this->email;
246
+ }
247
+ }
248
+
249
+ As an example you're going to check if the email in receipt payload matches the user email.
250
+ To validate the receipt, it is required to create the constraint first.
251
+ You only need to add the ``getTargets() `` method to the ``Constraint `` class::
252
+
253
+ // src/AppBundle/Validator/Constraints/ConfirmedPaymentReceipt.php
254
+ namespace AppBundle\Validator\Constraints;
255
+
256
+ use Symfony\Component\Validator\Constraint;
257
+
258
+ /**
259
+ * @Annotation
260
+ */
261
+ class ConfirmedPaymentReceipt extends Constraint
262
+ {
263
+ public $userDoesntMatchMessage = 'User email does not match the receipt email';
264
+
265
+ public function getTargets()
266
+ {
267
+ return self::CLASS_CONSTRAINT;
268
+ }
202
269
}
203
270
204
271
With this, the validator's ``validate() `` method gets an object as its first argument::
205
272
206
- class ProtocolClassValidator extends ConstraintValidator
273
+ // src/AppBundle/Validator/Constraints/ConfirmedPaymentReceiptValidator.php
274
+ namespace AppBundle\Validator\Constraints;
275
+
276
+ use Symfony\Component\Validator\Constraint;
277
+ use Symfony\Component\Validator\ConstraintValidator;
278
+ use Symfony\Component\Validator\Exception\UnexpectedValueException;
279
+
280
+ class ConfirmedPaymentReceiptValidator extends ConstraintValidator
207
281
{
208
- public function validate($protocol, Constraint $constraint)
282
+ /**
283
+ * @param PaymentReceipt $receipt
284
+ * @param Constraint|ConfirmedPaymentReceipt $constraint
285
+ */
286
+ public function validate($receipt, Constraint $constraint)
209
287
{
210
- if ($protocol->getFoo() != $protocol->getBar()) {
211
- $this->context->buildViolation($constraint->message)
212
- ->atPath('foo')
288
+ if (!$receipt instanceof PaymentReceipt) {
289
+ throw new UnexpectedValueException($receipt, PaymentReceipt::class);
290
+ }
291
+
292
+ if (!$constraint instanceof ConfirmedPaymentReceipt) {
293
+ throw new UnexpectedValueException($constraint, ConfirmedPaymentReceipt::class);
294
+ }
295
+
296
+ $receiptEmail = $receipt->getPayload()['email'] ?? null;
297
+ $userEmail = $receipt->getUser()->getEmail();
298
+
299
+ if ($userEmail !== $receiptEmail) {
300
+ $this->context
301
+ ->buildViolation($constraint->userDoesntMatchMessage)
302
+ ->atPath('user.email')
213
303
->addViolation();
214
304
}
215
305
}
@@ -232,47 +322,46 @@ not to the property:
232
322
namespace App\Entity;
233
323
234
324
use App\Validator as AcmeAssert;
235
-
325
+
236
326
/**
237
- * @AcmeAssert\ProtocolClass
327
+ * @AppAssert\ConfirmedPaymentReceipt
238
328
*/
239
- class AcmeEntity
329
+ class PaymentReceipt
240
330
{
241
331
// ...
242
332
}
243
333
244
334
.. code-block :: yaml
245
335
246
- # config/validator/ validation.yaml
247
- App\Entity\AcmeEntity :
336
+ # src/AppBundle/Resources/config/ validation.yml
337
+ AppBundle\Model\PaymentReceipt :
248
338
constraints :
249
- - App \Validator\ProtocolClass : ~
339
+ - AppBundle \Validator\Constraints\ConfirmedPaymentReceipt : ~
250
340
251
341
.. code-block :: xml
252
342
253
- <!-- config/validator /validation.xml -->
254
- <class name =" App\Entity\AcmeEntity " >
255
- <constraint name =" App \Validator\ProtocolClass " />
343
+ <!-- src/AppBundle/Resources/config /validation.xml -->
344
+ <class name =" AppBundle\Model\PaymentReceipt " >
345
+ <constraint name =" AppBundle \Validator\Constraints\ConfirmedPaymentReceipt " />
256
346
</class >
257
347
258
348
.. code-block :: php
259
349
260
- // src/Entity/AcmeEntity.php
261
- namespace App\Entity;
262
-
263
- use App\Validator\ProtocolClass;
350
+ // src/AppBundle/Model/PaymentReceipt.php
351
+ use AppBundle\Validator\Constraints\ConfirmedPaymentReceipt;
264
352
use Symfony\Component\Validator\Mapping\ClassMetadata;
265
353
266
- class AcmeEntity
354
+ class PaymentReceipt
267
355
{
268
356
// ...
269
357
270
358
public static function loadValidatorMetadata(ClassMetadata $metadata)
271
359
{
272
- $metadata->addConstraint(new ProtocolClass ());
360
+ $metadata->addConstraint(new ConfirmedPaymentReceipt ());
273
361
}
274
362
}
275
363
364
+ <<<<<<< HEAD
276
365
Testing Custom Constraints
277
366
--------------------------
278
367
@@ -315,3 +404,137 @@ unit tests for your custom constraints::
315
404
// ...
316
405
}
317
406
}
407
+
408
+ How to Unit Test your Validator
409
+ -------------------------------
410
+
411
+ To create a unit test for you custom validator, your test case class should
412
+ extend the ``ConstraintValidatorTestCase `` class and implement the ``createValidator() `` method::
413
+
414
+ protected function createValidator()
415
+ {
416
+ return new ContainsAlphanumericValidator();
417
+ }
418
+
419
+ After that you can add any test cases you need to cover the validation logic::
420
+
421
+ use AppBundle\Validator\Constraints\ContainsAlphanumeric;
422
+ use AppBundle\Validator\Constraints\ContainsAlphanumericValidator;
423
+ use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
424
+
425
+ class ContainsAlphanumericValidatorTest extends ConstraintValidatorTestCase
426
+ {
427
+ protected function createValidator()
428
+ {
429
+ return new ContainsAlphanumericValidator();
430
+ }
431
+
432
+ /**
433
+ * @dataProvider getValidStrings
434
+ */
435
+ public function testValidStrings($string)
436
+ {
437
+ $this->validator->validate($string, new ContainsAlphanumeric());
438
+
439
+ $this->assertNoViolation();
440
+ }
441
+
442
+ public function getValidStrings()
443
+ {
444
+ return [
445
+ ['Fabien'],
446
+ ['SymfonyIsGreat'],
447
+ ['HelloWorld123'],
448
+ ];
449
+ }
450
+
451
+ /**
452
+ * @dataProvider getInvalidStrings
453
+ */
454
+ public function testInvalidStrings($string)
455
+ {
456
+ $constraint = new ContainsAlphanumeric([
457
+ 'message' => 'myMessage',
458
+ ]);
459
+
460
+ $this->validator->validate($string, $constraint);
461
+
462
+ $this->buildViolation('myMessage')
463
+ ->setParameter('{{ string }}', $string)
464
+ ->assertRaised();
465
+ }
466
+
467
+ public function getInvalidStrings()
468
+ {
469
+ return [
470
+ ['example_'],
471
+ ['@$^&'],
472
+ ['hello-world'],
473
+ ['<body>'],
474
+ ];
475
+ }
476
+ }
477
+
478
+ You can also use the ``ConstraintValidatorTestCase `` class for creating test cases for class constraints::
479
+
480
+ use AppBundle\Validator\Constraints\ConfirmedPaymentReceipt;
481
+ use AppBundle\Validator\Constraints\ConfirmedPaymentReceiptValidator;
482
+ use Symfony\Component\Validator\Exception\UnexpectedValueException;
483
+ use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
484
+
485
+ class ConfirmedPaymentReceiptValidatorTest extends ConstraintValidatorTestCase
486
+ {
487
+ protected function createValidator()
488
+ {
489
+ return new ConfirmedPaymentReceiptValidator();
490
+ }
491
+
492
+ public function testValidReceipt()
493
+ {
494
+ $receipt = new PaymentReceipt(new User('[email protected] '), ['email' => '[email protected] ', 'data' => 'baz']);
495
+ $this->validator->validate($receipt, new ConfirmedPaymentReceipt());
496
+
497
+ $this->assertNoViolation();
498
+ }
499
+
500
+ /**
501
+ * @dataProvider getInvalidReceipts
502
+ */
503
+ public function testInvalidReceipt($paymentReceipt)
504
+ {
505
+ $this->validator->validate(
506
+ $paymentReceipt,
507
+ new ConfirmedPaymentReceipt(['userDoesntMatchMessage' => 'myMessage'])
508
+ );
509
+
510
+ $this->buildViolation('myMessage')
511
+ ->atPath('property.path.user.email')
512
+ ->assertRaised();
513
+ }
514
+
515
+ public function getInvalidReceipts()
516
+ {
517
+ return [
518
+ [new PaymentReceipt(new User('[email protected] '), [])],
519
+ [new PaymentReceipt(new User('[email protected] '), ['email' => '[email protected] '])],
520
+ ];
521
+ }
522
+
523
+ /**
524
+ * @dataProvider getUnexpectedArguments
525
+ */
526
+ public function testUnexpectedArguments($value, $constraint)
527
+ {
528
+ self::expectException(UnexpectedValueException::class);
529
+
530
+ $this->validator->validate($value, $constraint);
531
+ }
532
+
533
+ public function getUnexpectedArguments()
534
+ {
535
+ return [
536
+ [new \stdClass(), new ConfirmedPaymentReceipt()],
537
+ [new PaymentReceipt(new User('[email protected] '), []), new Unique()],
538
+ ];
539
+ }
540
+ }
0 commit comments