I’ve already written a post on how to create a custom validation constraint here (https://creativcoders.wordpress.com/2014/07/15/symfony2-tutorial-create-custom-constraint-validation-and-use-it-with-fosuserbundle/), but what if you need to validate two fields or more in your entity?
Well, I have an entity called “Card” that stores credit cards informations and i need to check the card validity by checking the expiration month and expiration year.
The proper way to do this is to create a custom validation constraint that we will append in our “card” entity.
This is how the entity looks:
<?php namespace EdouardKombo\StripePaymentBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; /** * Card * * @ORM\Table(name="headoo_stripe_credit_card") * @ORM\Entity */ class Card { /** * @var integer * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var integer * * @ORM\Column(name="number", type="integer") * @Assert\NotBlank(message="card.number.blank") * @Assert\Length(min="16", max="16", minMessage="card.number.length", maxMessage="card.number.length", exactMessage="card.number.length") * @Assert\Range(min="1000000000000000", max="9999999999999999", minMessage="card.number.range", maxMessage="card.number.range") * @Assert\Regex(pattern="^(([0-9]*)|(([0-9]*).([0-9]*)))$^", match=true, message="card.number.invalid") */ private $number; /** * @var integer * * @ORM\Column(name="expirationMonth", type="integer") * @Assert\NotBlank(message="card.expiration.month.blank") * @Assert\Length(min="2", max="2", minMessage="card.expiration.month.length", maxMessage="card.expiration.month.length", exactMessage="card.expiration.month.length") * @Assert\Range(min="1", max="12", minMessage="card.expiration.month.range", maxMessage="card.expiration.month.range") * @Assert\Regex(pattern="^(([0-9]*)|(([0-9]*).([0-9]*)))$^", match=true, message="card.expiration.month.invalid") */ private $expirationMonth; /** * @var integer * * @ORM\Column(name="expirationYear", type="integer") * @Assert\NotBlank(message="card.expiration.year.blank") * @Assert\Length(min="2", max="2", minMessage="card.expiration.year.length", maxMessage="card.expiration.year.length", exactMessage="card.expiration.year.length") * @Assert\Range(min="0", max="99", minMessage="card.expiration.year.range", maxMessage="card.expiration.month.range") * @Assert\Regex(pattern="^(([0-9]*)|(([0-9]*).([0-9]*)))$^", match=true, message="card.expiration.year.invalid") */ private $expirationYear; /** * @var integer * * @ORM\Column(name="cvc", type="integer") * @Assert\NotBlank(message="card.cvc.blank") * @Assert\Length(min="3", max="4", minMessage="card.cvc.short", maxMessage="card.cvc.long", exactMessage="card.cvc.long") * @Assert\Range(min="1", max="9999", minMessage="card.cvc.range", maxMessage="card.cvc.range") * @Assert\Regex(pattern="^(([0-9]*)|(([0-9]*).([0-9]*)))$^", match=true, message="card.cvc.invalid") */ private $cvc; /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set number * * @param integer $number * @return Card */ public function setNumber($number) { $this->number = $number; return $this; } /** * Get number * * @return integer */ public function getNumber() { return $this->number; } /** * Set expirationMonth * * @param integer $expirationMonth * @return Card */ public function setExpirationMonth($expirationMonth) { $this->expirationMonth = $expirationMonth; return $this; } /** * Get expirationMonth * * @return integer */ public function getExpirationMonth() { return $this->expirationMonth; } /** * Set expirationYear * * @param integer $expirationYear * @return Card */ public function setExpirationYear($expirationYear) { $this->expirationYear = $expirationYear; return $this; } /** * Get expirationYear * * @return integer */ public function getExpirationYear() { return $this->expirationYear; } /** * Set cvc * * @param integer $cvc * @return Card */ public function setCvc($cvc) { $this->cvc = $cvc; return $this; } /** * Get cvc * * @return integer */ public function getCvc() { return $this->cvc; } }
Ok so to validate the expirationMonth and expirationYear properties with custom validation constraints, we will create one constraint, called “CardHasExpired”.
- Create these files in your bundle main directory: “Validator/Constraints/CardHasExpired.php” and “Validator/Constraints/CardHasExpiredValidator.php”
This is how will look the CardHasExpired.php field:
<?php namespace EdouardKombo\StripePaymentBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation */ class CardHasExpired extends Constraint { /** * * @var string */ public $message = 'Your card has expired.'; /** * * @return string */ public function validatedBy() { return 'card_has_expired'; } /** * Get class constraints and properties * * @return array */ public function getTargets() { return array(self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT); } }
Now, we can open the CardHasExpiredValidator.php file and we can call the properties we want from our card entity.
This way we can validate multiple properties.
Here, i just need to check that the expiration month and year are not outdated.
This how our CardHasExpiredValidator.php file will look like.
<?php namespace EdouardKombo\StripePaymentBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; class CardHasExpiredValidator extends ConstraintValidator { /** * Method to validate * * @param string $value Property value * @param \Symfony\Component\Validator\Constraint $constraint All properties * * @return boolean */ public function validate($value, Constraint $constraint) { $date = getdate(); $year = (string) $date['year']; $month = (string) $date['mon']; $yearLastDigits = substr($year, 2); $monthLastDigits = $month; $otherFieldValue = $this->context->getRoot()->get('expirationMonth')->getData(); if (!empty($otherFieldValue) && ($value <= $yearLastDigits) && ($otherFieldValue <= $monthLastDigits)) { $this->context->addViolation( $constraint->message, array('%string%' => $value) ); return false; } return true; } }
Now, we have to create a service that will load our constraints.
<?php edouard_kombo_stripe_payment.card.validator.card_has_expired: class: EdouardKombo\StripePaymentBundle\Validator\Constraints\CardHasExpiredValidator tags: - { name: validator.constraint_validator, alias: card_has_expired }
And finally, we can use our constraint directly in our card entity, and assign it to $expirationYear property.
<?php namespace EdouardKombo\StripePaymentBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; use EdouardKombo\StripePaymentBundle\Validator\Constraints\CardHasExpired; /** * Card * * @ORM\Table(name="headoo_stripe_credit_card") * @ORM\Entity */ class Card { /** * @var integer * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var integer * * @ORM\Column(name="number", type="integer") * @Assert\NotBlank(message="card.number.blank") * @Assert\Length(min="16", max="16", minMessage="card.number.length", maxMessage="card.number.length", exactMessage="card.number.length") * @Assert\Range(min="1000000000000000", max="9999999999999999", minMessage="card.number.range", maxMessage="card.number.range") * @Assert\Regex(pattern="^(([0-9]*)|(([0-9]*).([0-9]*)))$^", match=true, message="card.number.invalid") */ private $number; /** * @var integer * * @ORM\Column(name="expirationMonth", type="integer") * @Assert\NotBlank(message="card.expiration.month.blank") * @Assert\Length(min="2", max="2", minMessage="card.expiration.month.length", maxMessage="card.expiration.month.length", exactMessage="card.expiration.month.length") * @Assert\Range(min="1", max="12", minMessage="card.expiration.month.range", maxMessage="card.expiration.month.range") * @Assert\Regex(pattern="^(([0-9]*)|(([0-9]*).([0-9]*)))$^", match=true, message="card.expiration.month.invalid") */ private $expirationMonth; /** * @var integer * * @ORM\Column(name="expirationYear", type="integer") * @Assert\NotBlank(message="card.expiration.year.blank") * @Assert\Length(min="2", max="2", minMessage="card.expiration.year.length", maxMessage="card.expiration.year.length", exactMessage="card.expiration.year.length") * @Assert\Range(min="0", max="99", minMessage="card.expiration.year.range", maxMessage="card.expiration.month.range") * @Assert\Regex(pattern="^(([0-9]*)|(([0-9]*).([0-9]*)))$^", match=true, message="card.expiration.year.invalid") * @CardHasExpired(message="card.expiration.expired") */ private $expirationYear; /** * @var integer * * @ORM\Column(name="cvc", type="integer") * @Assert\NotBlank(message="card.cvc.blank") * @Assert\Length(min="3", max="4", minMessage="card.cvc.short", maxMessage="card.cvc.long", exactMessage="card.cvc.long") * @Assert\Range(min="1", max="9999", minMessage="card.cvc.range", maxMessage="card.cvc.range") * @Assert\Regex(pattern="^(([0-9]*)|(([0-9]*).([0-9]*)))$^", match=true, message="card.cvc.invalid") */ private $cvc; /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set number * * @param integer $number * @return Card */ public function setNumber($number) { $this->number = $number; return $this; } /** * Get number * * @return integer */ public function getNumber() { return $this->number; } /** * Set expirationMonth * * @param integer $expirationMonth * @return Card */ public function setExpirationMonth($expirationMonth) { $this->expirationMonth = $expirationMonth; return $this; } /** * Get expirationMonth * * @return integer */ public function getExpirationMonth() { return $this->expirationMonth; } /** * Set expirationYear * * @param integer $expirationYear * @return Card */ public function setExpirationYear($expirationYear) { $this->expirationYear = $expirationYear; return $this; } /** * Get expirationYear * * @return integer */ public function getExpirationYear() { return $this->expirationYear; } /** * Set cvc * * @param integer $cvc * @return Card */ public function setCvc($cvc) { $this->cvc = $cvc; return $this; } /** * Get cvc * * @return integer */ public function getCvc() { return $this->cvc; } }
Congratulations, you’ve done it, you’re a champion.
Hi, this here is not true / needed at all:
“Now, we have to create a service that will load our constraints.”
You only do that when you need to inject params into your Validator class, which you are not doing.
(http://symfony.com/doc/current/cookbook/validation/custom_constraint.html)
nova.cp