From 47c531494fc79f067ff23771f47fda8a3f006587 Mon Sep 17 00:00:00 2001 From: Andrii Popov Date: Wed, 24 Jun 2020 23:35:27 +0300 Subject: [PATCH 1/6] add section --- validation/custom_constraint.rst | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/validation/custom_constraint.rst b/validation/custom_constraint.rst index 0540ecfb4d6..243858a1914 100644 --- a/validation/custom_constraint.rst +++ b/validation/custom_constraint.rst @@ -250,3 +250,73 @@ not to the property: $metadata->addConstraint(new ProtocolClass()); } } + +How to Unit Test your Validator +------------------------------- + +To create a unit test for you custom validator, you can use ``ConstraintValidatorTestCase`` class. All you need is to extend +from it and implement the ``createValidator`` method. This method must return an instance of your custom constraint validator class:: + + protected function createValidator() + { + return new ContainsAlphanumericValidator(); + } + +After that you can add any test cases you need to cover the validation logic:: + + use App\ContainsAlphanumeric; + use App\ContainsAlphanumericValidator; + use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + + class ContainsAlphanumericValidatorTest extends ConstraintValidatorTestCase + { + protected function createValidator() + { + return new ContainsAlphanumericValidator(); + } + + /** + * @dataProvider getValidStrings + */ + public function testValidStrings($string) + { + $this->validator->validate($string, new ContainsAlphanumeric()); + + $this->assertNoViolation(); + } + + public function getValidStrings() + { + return [ + ['Fabien'], + ['SymfonyIsGreat'], + ['HelloWorld123'], + ]; + } + + /** + * @dataProvider getInvalidStrings + */ + public function testInvalidStrings($string) + { + $constraint = new ContainsAlphanumeric([ + 'message' => 'myMessage', + ]); + + $this->validator->validate($string, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ string }}', $string) + ->assertRaised(); + } + + public function getInvalidStrings() + { + return [ + ['example_'], + ['@$^&'], + ['hello-world'], + [''], + ]; + } + } From 76337d2e99bad624bca96d5f58bb42812cb7d21b Mon Sep 17 00:00:00 2001 From: Andrii Popov Date: Wed, 24 Jun 2020 23:44:24 +0300 Subject: [PATCH 2/6] add missing brackets --- validation/custom_constraint.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validation/custom_constraint.rst b/validation/custom_constraint.rst index 243858a1914..133c1e8aaca 100644 --- a/validation/custom_constraint.rst +++ b/validation/custom_constraint.rst @@ -255,7 +255,7 @@ How to Unit Test your Validator ------------------------------- To create a unit test for you custom validator, you can use ``ConstraintValidatorTestCase`` class. All you need is to extend -from it and implement the ``createValidator`` method. This method must return an instance of your custom constraint validator class:: +from it and implement the ``createValidator()`` method. This method must return an instance of your custom constraint validator class:: protected function createValidator() { From 3300e5229674b86c2af2e76e9a0744e87c7dc337 Mon Sep 17 00:00:00 2001 From: Andrii Popov Date: Thu, 25 Jun 2020 00:21:38 +0300 Subject: [PATCH 3/6] fix namespace --- validation/custom_constraint.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/validation/custom_constraint.rst b/validation/custom_constraint.rst index 133c1e8aaca..2ae8f52a877 100644 --- a/validation/custom_constraint.rst +++ b/validation/custom_constraint.rst @@ -264,8 +264,8 @@ from it and implement the ``createValidator()`` method. This method must return After that you can add any test cases you need to cover the validation logic:: - use App\ContainsAlphanumeric; - use App\ContainsAlphanumericValidator; + use AppBundle\Validator\Constraints\ContainsAlphanumeric; + use AppBundle\Validator\Constraints\ContainsAlphanumericValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; class ContainsAlphanumericValidatorTest extends ConstraintValidatorTestCase From d03307816b2f7110940ef0597297d60d4c7b31a6 Mon Sep 17 00:00:00 2001 From: Andrii Popov Date: Fri, 26 Jun 2020 12:51:36 +0300 Subject: [PATCH 4/6] shorten paragraph --- validation/custom_constraint.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/validation/custom_constraint.rst b/validation/custom_constraint.rst index 2ae8f52a877..f38a60249c5 100644 --- a/validation/custom_constraint.rst +++ b/validation/custom_constraint.rst @@ -254,8 +254,8 @@ not to the property: How to Unit Test your Validator ------------------------------- -To create a unit test for you custom validator, you can use ``ConstraintValidatorTestCase`` class. All you need is to extend -from it and implement the ``createValidator()`` method. This method must return an instance of your custom constraint validator class:: +To create a unit test for you custom validator, your test case class should +extend the ``ConstraintValidatorTestCase`` class and implement the ``createValidator()`` method:: protected function createValidator() { From 7f92f2637c242c7e51ece35b0b06ee0769f8a52b Mon Sep 17 00:00:00 2001 From: Andrii Popov Date: Sun, 27 Sep 2020 10:31:13 +0300 Subject: [PATCH 5/6] add receipt validator snippets --- validation/custom_constraint.rst | 191 ++++++++++++++++++++++++++++--- 1 file changed, 173 insertions(+), 18 deletions(-) diff --git a/validation/custom_constraint.rst b/validation/custom_constraint.rst index f38a60249c5..f7b42bc03c8 100644 --- a/validation/custom_constraint.rst +++ b/validation/custom_constraint.rst @@ -179,22 +179,113 @@ Class Constraint Validator ~~~~~~~~~~~~~~~~~~~~~~~~~~ Besides validating a single property, a constraint can have an entire class -as its scope. You only need to add this to the ``Constraint`` class:: +as its scope. Consider the following classes, that describe the receipt of some payment:: - public function getTargets() + // src/AppBundle/Model/PaymentReceipt.php + + class PaymentReceipt + { + /** + * @var User + */ + private $user; + + /** + * @var array + */ + private $payload; + + public function __construct(User $user, array $payload) + { + $this->user = $user; + $this->payload = $payload; + } + + public function getUser(): User + { + return $this->user; + } + + public function getPayload(): array + { + return $this->payload; + } + } + + // src/AppBundle/Model/User.php + + class User + { + /** + * @var string + */ + private $email; + + public function __construct($email) + { + $this->email = $email; + } + + public function getEmail(): string + { + return $this->email; + } + } + +As an example you're going to check if the email in receipt payload matches the user email. +To validate the receipt, it is required to create the constraint first. +You only need to add the ``getTargets()`` method to the ``Constraint`` class:: + + // src/AppBundle/Validator/Constraints/ConfirmedPaymentReceipt.php + namespace AppBundle\Validator\Constraints; + + use Symfony\Component\Validator\Constraint; + + /** + * @Annotation + */ + class ConfirmedPaymentReceipt extends Constraint { - return self::CLASS_CONSTRAINT; + public $userDoesntMatchMessage = 'User email does not match the receipt email'; + + public function getTargets() + { + return self::CLASS_CONSTRAINT; + } } With this, the validator's ``validate()`` method gets an object as its first argument:: - class ProtocolClassValidator extends ConstraintValidator + // src/AppBundle/Validator/Constraints/ConfirmedPaymentReceiptValidator.php + namespace AppBundle\Validator\Constraints; + + use Symfony\Component\Validator\Constraint; + use Symfony\Component\Validator\ConstraintValidator; + use Symfony\Component\Validator\Exception\UnexpectedValueException; + + class ConfirmedPaymentReceiptValidator extends ConstraintValidator { - public function validate($protocol, Constraint $constraint) + /** + * @param PaymentReceipt $receipt + * @param Constraint|ConfirmedPaymentReceipt $constraint + */ + public function validate($receipt, Constraint $constraint) { - if ($protocol->getFoo() != $protocol->getBar()) { - $this->context->buildViolation($constraint->message) - ->atPath('foo') + if (!$receipt instanceof PaymentReceipt) { + throw new UnexpectedValueException($receipt, PaymentReceipt::class); + } + + if (!$constraint instanceof ConfirmedPaymentReceipt) { + throw new UnexpectedValueException($constraint, ConfirmedPaymentReceipt::class); + } + + $receiptEmail = $receipt->getPayload()['email'] ?? null; + $userEmail = $receipt->getUser()->getEmail(); + + if ($userEmail !== $receiptEmail) { + $this->context + ->buildViolation($constraint->userDoesntMatchMessage) + ->atPath('user.email') ->addViolation(); } } @@ -214,9 +305,9 @@ not to the property: .. code-block:: php-annotations /** - * @AcmeAssert\ProtocolClass + * @AppAssert\ConfirmedPaymentReceipt */ - class AcmeEntity + class PaymentReceipt { // ... } @@ -224,30 +315,30 @@ not to the property: .. code-block:: yaml # src/AppBundle/Resources/config/validation.yml - AppBundle\Entity\AcmeEntity: + AppBundle\Model\PaymentReceipt: constraints: - - AppBundle\Validator\Constraints\ProtocolClass: ~ + - AppBundle\Validator\Constraints\ConfirmedPaymentReceipt: ~ .. code-block:: xml - - + + .. code-block:: php - // src/AppBundle/Entity/AcmeEntity.php - use AppBundle\Validator\Constraints\ProtocolClass; + // src/AppBundle/Model/PaymentReceipt.php + use AppBundle\Validator\Constraints\ConfirmedPaymentReceipt; use Symfony\Component\Validator\Mapping\ClassMetadata; - class AcmeEntity + class PaymentReceipt { // ... public static function loadValidatorMetadata(ClassMetadata $metadata) { - $metadata->addConstraint(new ProtocolClass()); + $metadata->addConstraint(new ConfirmedPaymentReceipt()); } } @@ -320,3 +411,67 @@ After that you can add any test cases you need to cover the validation logic:: ]; } } + +You can also use the ``ConstraintValidatorTestCase`` class for creating test cases for class constraints:: + + use AppBundle\Validator\Constraints\ConfirmedPaymentReceipt; + use AppBundle\Validator\Constraints\ConfirmedPaymentReceiptValidator; + use Symfony\Component\Validator\Exception\UnexpectedValueException; + use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + + class ConfirmedPaymentReceiptValidatorTest extends ConstraintValidatorTestCase + { + protected function createValidator() + { + return new ConfirmedPaymentReceiptValidator(); + } + + public function testValidReceipt() + { + $receipt = new PaymentReceipt(new User('foo@bar.com'), ['email' => 'foo@bar.com', 'data' => 'baz']); + $this->validator->validate($receipt, new ConfirmedPaymentReceipt()); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getInvalidReceipts + */ + public function testInvalidReceipt($paymentReceipt) + { + $this->validator->validate( + $paymentReceipt, + new ConfirmedPaymentReceipt(['userDoesntMatchMessage' => 'myMessage']) + ); + + $this->buildViolation('myMessage') + ->atPath('property.path.user.email') + ->assertRaised(); + } + + public function getInvalidReceipts() + { + return [ + [new PaymentReceipt(new User('foo@bar.com'), [])], + [new PaymentReceipt(new User('foo@bar.com'), ['email' => 'baz@foo.com'])], + ]; + } + + /** + * @dataProvider getUnexpectedArguments + */ + public function testUnexpectedArguments($value, $constraint) + { + self::expectException(UnexpectedValueException::class); + + $this->validator->validate($value, $constraint); + } + + public function getUnexpectedArguments() + { + return [ + [new \stdClass(), new ConfirmedPaymentReceipt()], + [new PaymentReceipt(new User('foo@bar.com'), []), new Unique()], + ]; + } + } From a8e8efeabf1e9a6f4b3eb49edd49d64015e1b274 Mon Sep 17 00:00:00 2001 From: Andrii Popov Date: Sun, 27 Sep 2020 10:36:08 +0300 Subject: [PATCH 6/6] fix cs --- validation/custom_constraint.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/validation/custom_constraint.rst b/validation/custom_constraint.rst index f7b42bc03c8..06aa602011e 100644 --- a/validation/custom_constraint.rst +++ b/validation/custom_constraint.rst @@ -182,7 +182,6 @@ Besides validating a single property, a constraint can have an entire class as its scope. Consider the following classes, that describe the receipt of some payment:: // src/AppBundle/Model/PaymentReceipt.php - class PaymentReceipt { /**