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.

Advertisements
Symfony2: two fields comparison with custom validation constraints

Symfony2 tutorial: Create custom constraint validation and use it with FOSUserBundle

Today, we will learn how to create a custom validation constraint with Symfony2 and how to use it with FOSUserBundle.
Let’s start.

We have a user registration form in which we want the username to be automatically validated.
Create your formType and your controller classically, we will focus on constraints.

Let’s name our new constraint: “ContainsUser”.
Create a “ContainsUser.php” file in Validator/Contraints directory at your bundle root.

This file should look like this:

<?php

namespace VendorName\UserBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 */
class ContainsUser extends Constraint
{
    public $message = 'The user "%string%" already exists.';
    
    
    public function validatedBy()
    {
        return 'contains_user';
    }

    public function getTargets()
    {
        return array(self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT);
    }    
}
  • ContainsUser will be our constraint.
  • ValidatedBy() function returns an alias that will help defines a service.
  • In getTargets() function, we specify the targets linked to our constraint (properties and/or classes).

Ok, now we have defined our constraint, we need to create the constraintValidator, it is the file containing the validation logic we want.
Create a “ContainsUserValidator.php” file, always in Validator/Constraints directory.

The file should like this:

<?php

namespace VendorName\UserBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class ContainsUserValidator extends ConstraintValidator
{
    protected $em;
    
    public function __construct(\Doctrine\ORM\EntityManager $em)
    {
        $this->em = $em;
    }
    
    public function validate($value, Constraint $constraint)
    {
        $repository     = $this->em->getRepository('HeadooUserBundle:User');
        $user           = $repository->findOneByUsername($value);
        
        if ($user) {
            $this->context->addViolation(
                $constraint->message,
                array('%string%' => $value)
            );
        }
    }
}

We only loaded a Doctrine Orm dependency and, in the “validate” method, simply checked if the user already exists and generate a context violation.

In order to make it work, we must create a service that injects specified dependencies, just like that:

    vendorname.user.validator.contains_user:
        class: VendorName\UserBundle\Validator\Constraints\ContainsUserValidator
        arguments:
            - @doctrine.orm.entity_manager        
        tags:
            - { name: validator.constraint_validator, alias: contains_user }  

Note that the alias must match the validateBy function return value in our constraint class.

Ok, we have now a constraint with a working validator.
The last thing to do is to call the constraint in FOSUserBundle validation file.
You must override the FOSUserBundle “validation.xml” file in your “Resources/config” directory.

We just need to add our constraint for the username property, so we only keep in this file, the username property constraints.

This is how our constraint must looks like:

<constraint name="VendorName\UserBundle\Validator\Constraints\ContainsUser">
    <option name="message">fos_user.username.already_used</option>
    <option name="groups">
        <value>Registration</value>
        <value>Profile</value>
    </option>                
</constraint>

if you want to keep the message specified in the constraint class, just delete the name=’message’ option.

The complete file must look like this.

<?xml version="1.0" ?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping
        http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">  
    
    <class name="FOS\UserBundle\Model\User">

        <property name="username">
            <constraint name="NotBlank">
                <option name="message">fos_user.username.blank</option>
                <option name="groups">
                    <value>Registration</value>
                    <value>Profile</value>
                </option>
            </constraint>
            <constraint name="Headoo\UserBundle\Validator\Constraints\ContainsUser">
                <option name="message">fos_user.username.already_used</option>
                <option name="groups">
                    <value>Registration</value>
                    <value>Profile</value>
                </option>                
            </constraint>                      
            <constraint name="Length">
                <option name="min">2</option>
                <option name="minMessage">fos_user.username.short</option>
                <option name="max">255</option>
                <option name="maxMessage">fos_user.username.long</option>
                <option name="groups">
                    <value>Registration</value>
                    <value>Profile</value>
                </option>
            </constraint>
        </property>
    </class>

</constraint-mapping>

And that’s it, you have a fully functional customed constraint validation.

Have fun !

Symfony2 tutorial: Create custom constraint validation and use it with FOSUserBundle

Symfony2 tutorial: Log a user manually by creating your own listener

I had to develop a user registration form with specific roles. The problem was that the form was too big and needed to be simplified.
I then decided to create my own bundle for multi-step registrations, and I needed to log a user manually after form registration, and I want to share what I’ve learned with you.

This is how you can perform it.
First of all, you have to know some symfony base principles:
– Current user informations are stored in the security context token, and, can be retrieved from here.
– The security context token listens to security interactiveLogin event that requires user object and token.

So, to log manually a user, we have to:
– create a user object and generate a token (we will use UsernamePasswordToken for that).
– Listen to interactiveLogin event and sends him the token with the current request
– Dispatch InteractiveLogin event to the security context.

Right, now let’s go!

Instead of creating a controller, we’ll create a listener “LoginListener” in a Listener directory.
This is how our code will looks like.

<?php

/**
 * Main docblock
 *
 * PHP version 5
 *
 * @category  Listener
 * @package   MultiStepFormsBundle
 * @author    Edouard Kombo <edouard.kombo@gmail.com>
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 * @version   GIT: 1.0.0
 * @link      https://creativcoders.wordpress.com
 * @since     0.0.0
 */
namespace VendorName\UserBundle\Listener;

use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\SecurityContext;

/**
 * Listen to interactive login event and log in a user manually
 *
 * @category Listener
 * @package  MultiStepFormsBundle
 * @author   Edouard Kombo <edouard.kombo@gmail.com>
 * @license  http://www.opensource.org/licenses/mit-license.php MIT License
 * @link     https://creativcoders.wordpress.com
 */
class LoginListener
{
    /** 
     * @var \Symfony\Component\Security\Core\SecurityContext 
     */
    protected $securityContext;
        
    /**
     *
     * @var string $username
     */
    protected $username;
    
    /**
     *
     * @var object $em
     */
    protected $em;
    
    /**
     *
     * @var string $firewall
     */
    protected $firewall; 
    
    /**
     *
     * @var string $userEntity
     */
    protected $userEntity;     
    
    /**
     *
     * @var object $container
     */
    protected $container;    
    
    /**
     * Constructor
     * 
     * @param \Doctrine\ORM\EntityManager $em              Doctrine orm
     * @param object                      $container       Container Object
     * @param SecurityContext             $securityContext Security Management
     * 
     * @return \EdouardKombo\MultiStepFormsBundle\Listener\LoginListener
     */
    public function __construct(\Doctrine\ORM\EntityManager $em, $container, 
            SecurityContext $securityContext)
    {
        $this->em               = (object) $em; 
        $this->container        = (object) $container;
        $this->securityContext  = (object) $securityContext;
        
        return $this;
    }
    
    /**
     * Set the username
     * 
     * @param string $username Username of the current user
     * 
     * @return \EdouardKombo\MultiStepFormsBundle\Listener\LoginListener
     */
    public function setUsername($username)
    {
        $this->username = (string) $username;
        return $this;
    }
    
    /**
     * Set the firewall
     * 
     * @param string $firewall Actual security firewall
     * 
     * @return \EdouardKombo\MultiStepFormsBundle\Listener\LoginListener
     */
    public function setFirewall($firewall)
    {
        $this->firewall = (string) $firewall;
        return $this;
    }
    
    /**
     * Define the user entity
     * 
     * @param string $userEntity Project User entity
     * 
     * @return \EdouardKombo\MultiStepFormsBundle\Listener\LoginListener
     */
    public function setUserEntity($userEntity)
    {
        $this->userEntity = (string) $userEntity;
        return $this;
    }    
    
    /**
     * Login a user and dispatch the event.
     * 
     * @return object
     * @throws UsernameNotFoundException
     */
    public function secureInteractiveLogin()
    {
        if (!$this->securityContext->isGranted('IS_AUTHENTICATED_FULLY')) {
            
            $repository  = $this->em->getRepository($this->userEntity);       
            $user        = $repository->findOneByUsername($this->username);

            $request     = $this->container->get('request');
                
            if (!$user) {
                throw new UsernameNotFoundException("User not found");

            } else {
                //We need the name of the firewall
                $token = new UsernamePasswordToken($user, null, $this->firewall, 
                    $user->getRoles());

                $this->securityContext->setToken($token);

                //Now, login the user
                $event = new InteractiveLoginEvent($request, $token);
                return $this->container->get("event_dispatcher")->dispatch(
                    "security.interactive_login", 
                    $event
                );
            }            
        } else {
            $user = $event->getAuthenticationToken()->getUser();
        }
        
        return $user;
    }    
}

Last step is to create a service to inject dependencies on object construct.

//Change depending on your needs
services:
    multistep_forms.login_listener:
        class: %multistep_forms.login_listener.class%
        arguments:
            - @doctrine.orm.entity_manager
            - @service_container
            - @security.context
Symfony2 tutorial: Log a user manually by creating your own listener