@@ -194,22 +194,112 @@ Class Constraint Validator
194194~~~~~~~~~~~~~~~~~~~~~~~~~~
195195
196196Besides 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 ::
198198
199- public function getTargets()
199+ // src/AppBundle/Model/PaymentReceipt.php
200+ class PaymentReceipt
200201 {
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+ }
202269 }
203270
204271With this, the validator's ``validate() `` method gets an object as its first argument::
205272
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
207281 {
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)
209287 {
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')
213303 ->addViolation();
214304 }
215305 }
@@ -232,47 +322,46 @@ not to the property:
232322 namespace App\Entity;
233323
234324 use App\Validator as AcmeAssert;
235-
325+
236326 /**
237- * @AcmeAssert\ProtocolClass
327+ * @AppAssert\ConfirmedPaymentReceipt
238328 */
239- class AcmeEntity
329+ class PaymentReceipt
240330 {
241331 // ...
242332 }
243333
244334 .. code-block :: yaml
245335
246- # config/validator/ validation.yaml
247- App\Entity\AcmeEntity :
336+ # src/AppBundle/Resources/config/ validation.yml
337+ AppBundle\Model\PaymentReceipt :
248338 constraints :
249- - App \Validator\ProtocolClass : ~
339+ - AppBundle \Validator\Constraints\ConfirmedPaymentReceipt : ~
250340
251341 .. code-block :: xml
252342
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 " />
256346 </class >
257347
258348 .. code-block :: php
259349
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;
264352 use Symfony\Component\Validator\Mapping\ClassMetadata;
265353
266- class AcmeEntity
354+ class PaymentReceipt
267355 {
268356 // ...
269357
270358 public static function loadValidatorMetadata(ClassMetadata $metadata)
271359 {
272- $metadata->addConstraint(new ProtocolClass ());
360+ $metadata->addConstraint(new ConfirmedPaymentReceipt ());
273361 }
274362 }
275363
364+ <<<<<<< HEAD
276365Testing Custom Constraints
277366--------------------------
278367
@@ -315,3 +404,137 @@ unit tests for your custom constraints::
315404 // ...
316405 }
317406 }
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('foo@bar.com'), ['email' => 'foo@bar.com', '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('foo@bar.com'), [])],
519+ [new PaymentReceipt(new User('foo@bar.com'), ['email' => 'baz@foo.com'])],
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('foo@bar.com'), []), new Unique()],
538+ ];
539+ }
540+ }
0 commit comments