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: 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

Symfony2 differences between doctrine:schema:update and doctrine:migrations

When you have to create or update your database schema from entities, you are used to type these commands:

php app/console doctrine:schema:update --dump-sql
php app/console doctrine:schema:update --force

Doctrine offers another extension called Migrations that helps you:
– Save your different database schemas during the life of your project
– Move tables and databases with their content, what a simple doctrine:schema:update can’t do.
– Generate accessible migrations queries from the difference between your entities and your database schema.
– Include your own migration queries.

To use doctrine migrations, you will often use these commands:

php app/console doctrine:migrations:diff #Generate migrations queries
php app/console doctrine:migrations:migrate #Refresh your database schema from migrations queries

Finally, for rapid schema updates, use doctrine:schema:update, but, for complex modifications in your database, prefer doctrine:migrations.

Symfony2 differences between doctrine:schema:update and doctrine:migrations

Symfony2 // [Symfony\Component\DependencyInjection\Exception\InvalidArgumentException] unable to parse file…

When reorganizing my bundles, I decided to load my services from my bundle dependency injection.
My bundle name is “UserBundle” and my vendor name “xxx”.

So, I created an extension file from my bundle called “xxxUserExtension.php” in “UserBundle/DependencyInjection/”

This was my content file:

<?php

namespace xxx\UserBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;

/**
 * This is the class that loads and manages your bundle configuration
 *
 * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
 */
class xxxUserExtension extends Extension
{
    /**
     * {@inheritDoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('user.yml');
        $loader->load('forms.yml');        
        $loader->load('validation.xml');       
    }
}

And of course, I’ve got this error:

    [Symfony\Component\DependencyInjection\Exception\InvalidArgumentException]
    Unable to parse file "C:\wamp\www\xxx\src\Headoo\UserBundle\DependencyInjection/../Resources/config\user.yml".

Maybe you want to know the cause, right?

This error simply occured because I was loading an yml file with an xmlFileLoader. Also, i was loading an xml file, we never have to load two different file types with a single file type loader.

Look here:

        $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('user.yml');
        $loader->load('forms.yml');
        $loader->load('validation.xml'); 

So to solve this error, you have to use only one extension for your services and choose the correct loader.
This is the correct file:

<?php

namespace xxx\UserBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;

/**
 * This is the class that loads and manages your bundle configuration
 *
 * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
 */
class xxxUserExtension extends Extension
{
    /**
     * {@inheritDoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('user.yml');
        $loader->load('forms.yml');               
    }
}

Hope, this helped.

Symfony2 // [Symfony\Component\DependencyInjection\Exception\InvalidArgumentException] unable to parse file…

Symfony2 // How to organize your bundles

When creating a new project, you probably already asked yourself this question, how can I organize my bundles?

The answer is that there is no standard good practice, it all depends of the nature of your project.

For small projects in which you don’t need to reuse some codes, you can decide to put all your sources in a single “appBundle”.
By against, for bigger projects that are frequently evolving, it is a good practice to organize more properly your bundles.

Usually we proceed like this: one bundle per module and one controller per functionality.
So, you’ll have to clearly identify your modules and make them bundles like this:

- MySiteName
    - UserBundle
        - RegistrationController
        - ChangePasswordController
    - ContactBundle
        - ContactController
    - PaymentBundle
        - PaypalController
        - MoneyBookerController

One advantage of this method is that you could publicly share some bundles to the community in public platforms.

More informations on the official Symfony Doc: http://symfony.com/doc/current/quick_tour/the_architecture.html

Symfony2 // How to organize your bundles

Symfony2 // Differences between Roles and Acl

Let’s imagine you have a blog with two users (Ana and Edouard), and want to manage specific actions like:

  • authorize Edouard to add a new contributor to the blog
  • authorize Ana to create, edit and delete her own blog post
  • authorize Edouard to delete Ana’s blog post

What is a Role

A role is a set of permissions hard coded in your application, and you can hard code yourselves. in your application. When checking if Edouard is able to delete Ana’s blog, your application checks in your code the current role the user has..

ACLs

Access Control Lists are useful when you need to take an authorization decision based on a Role + a domain object. Explicitely, they allow to give specific permissions to a specific object for a specific user.

Ana is allowed to edit blog entries written by her only. To check this authorization, you need Ana’s Roles and the Post model she’s trying to edit.

So, Edouard is allowed to all blog entries because he has the ROLE_ADMIN. The decision here is only based on a Role.

 

See Symfony documentation on Security/Roles and Symfony’s documentation on ACLs.

Symfony2 // Differences between Roles and Acl