Symfony2: two fields comparison with custom validation constraints

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.

Advertisement
Symfony2: two fields comparison with custom validation constraints

One thought on “Symfony2: two fields comparison with custom validation constraints

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s