Create your own constraint validator in symfony2 : A Doctrine unique validator

The new symfony2 form framework is definitely one of my favorites. It is quite simple, and very powerful and straightforward. Moreover it is very extensible.

One of the most common tasks you need to perform when validating a form is to check the unity of a field in your database. Unfortunately, there is no build-in validator coupled with doctrine. Here is my implementation.

Create the unique validator

The first thing to do when implementing a new validator is to create the constraint class:
[cc lang="php"]
namespace MyAppMyBundleValidator;

use SymfonyComponentValidatorConstraint;

class Unique extends Constraint
{
public $message = ‘This value is already used’;
public $entity;
public $property;

public function validatedBy()
{
return ‘validator.unique’;
}

public function requiredOptions()
{
return array(‘entity’, ‘property’);
}

public function targets()
{
return self::PROPERTY_CONSTRAINT;
}
}
[/cc]

As you can see, this definition is pretty simple. We define the error message and two more required options which are the Entity that doctrine with check and its property. The validatedBy() method returns the service’s name that is in charge to validate the constraint (we will declare it just after). Finally, the targets() function limits the use of the constraint only to class’ property.

Now that our constraint is set, let’s create the constraint’s validator:
[cc lang="php"]
namespace MyAppMyBundleValidator;

use DoctrineORMEntityManager;
use SymfonyComponentValidatorConstraint;
use SymfonyComponentValidatorConstraintValidator;

class UniqueValidator extends ConstraintValidator
{
private $entityManager;

public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}

public function isValid($value, Constraint $constraint) {
// try to get one entity that matches the constraint
$user = $this->entityManager->getRepository($constraint->entity)
->findOneBy(array($constraint->property => $value));
// if there is already an entity
if($user != null){
// the constraint does not pass
$this->setMessage($constraint->message);
return false;
}
// the constraint passes
return true;
}
}
[/cc]

The main function here is isValid(). It checks the validity of the constraint and consequently returns true/false and may add some error message. Here our job is to check is the value is already used in the database. The simplest way to do that is to try to get one matching entity. If Doctrine finds an entity the constraints does not pass, if there is no entity, the value can be used and the constraint passes.

One thing to know is that the Unique constraint instance is pass to the isValid() function, so that we can get the entity et property values.

Finally, our UniqueValidator has a dependency to the Doctrine manager. We will need to declare it in our services:

[cc lang="yaml"]
# MyApp/MyBundle/Resources/config/services.yml
parameters:
my.validator.unique.class: MyAppMyBundleValidatorUniqueValidator

services:
my.validator.unique:
class: %my.validator.unique.class%
arguments: [@doctrine.orm.entity_manager]
tags:
– { name: validator.constraint_validator, alias: validator.unique }
[/cc]

We defined here the way to instantiate our UniqueValidator with its dependencies (arguments line). The tag line is very important. The name specifies that it is load as a constraint validator, and the alias name is used to link the constraint validator to its constraint (remember the Unique::validatedBy() function).
One last thing is to load our services.yml file. We can do it in the dependency injection configuration :
[cc lang="php"]
load(‘services.yml’);
}

public function getAlias() {
return ‘my’;
}
}
[/cc]

The load() function is launched at the configuration of the container. Here we simple retrieve the services.yml file and load it to the current configuration.

That’s it you know have a working constraint that you can use to check the unity of a field at form’s submission. Don’t forget to set the entity and property values.

Support annotation

Actually I prefer setting my constraints using annotations. To do so, we will need some more work.

Let’s say we want to use our constraint like that:
[cc lang="php"]
/**
* @orm:Entity
*/
class User{
/**
* @orm:Column(length=255, unique=”TRUE”)
* @myvalidation:Unique(entity=”MyBundle:User”, property=”username”)
*/
protected $username;

//…
}
[/cc]

It won’t work right away. This is because the myvalidation is a shortcut to a namespace (just like validation or orm). Because we did not declare this shortcut, symfony cannot find the Unique class and simply ignores it.

To declare the myvalidation shortcut you can do it directly in the app/config/config.yml. I chose to do it in the bundle because I don’t like to play so much with the app configuration. One of the benefits of bundle is that they decoupled the application, so we don’t want our general configuration to become dependent to our bundle.

To declare the shortcut in our bundle we can do it in the dependency injection configuration:
[cc lang="php"]
load(‘services.yml’);

// get the existing registered namespaces for validator annotations
$namespaces = $container->getParameter(‘validator.annotations.namespaces’);
// add our namespace under the alias myvalidation
$namespaces['myvalidation'] = ‘MyApp\MyBundle\Validator\’;
// save it
$container->setParameter(‘validator.annotations.namespaces’, $namespaces);
}

public function getAlias() {
return ‘my’;
}
}
[/cc]

What we need to do is to retrieve the namespace aliases of the validators and then add our alias. Don’t forget the trailing backslashes.

And voila! Our Unique constraint works also with annotations.

I hope that this tutorial will help you to build new powerful constraints and share them to the community.

About these ads

16 thoughts on “Create your own constraint validator in symfony2 : A Doctrine unique validator

  1. Alex says:

    Hello.

    Thank you for the great tutorial.

    Just one notice: it seems this code prevents form’s updating, in case user doesn’t change value of the field, that has to be unique.

    Example:
    There is a table with fields:
    - id – simple primary key;
    - unique_name – field, that is designed to be unique.

    Steps:
    1. Create new entry with “unique_name” – it works fine.
    2. Try to edit this entry’s attributes, but don’t change “unique_name” value- this action fails, because method
    findOneBy(array($constraint->property => $value)) finds currently edited entry. As a result, false is returned and there is no option to update entry without changing value of the unique field.

      • Hi,

        I checked this issue these past days, while pushing the validator to the git. And it seems like my implementation that supported update was not a so good idea.
        It was using the binded object, assuming it was an entity, and comparing it with the entity retrieved by doctrine. If the entity was the same, it was considering that the entity was being updated and would not return false.
        It might look like a good idea, but what if you use the validator on a object that is not an entity.
        So I should find a way to tell the constraint that one specific entity can be considered as an updated one. And I don’t know how to do it yet…

  2. Hi Michel,

    Thank you for this tutorial.
    Have you found a solution to troubleshoot the problem when editing ?

    I may have a solution but I’m not sure. Do you think we could pass the ID of the entity to the constraint so that we can check the value of ID before trying to find the entity.
    If findOneBy returns an entity and ID is not null and $entity->id = ID so we can considerate returning true.

    I do not know if I’m clear, do you think this could work ?

      • You’re true, I have checked all the core validators and none of them specifies a validator that check a condition between two properties of the same entity.

  3. @Nek

    The answer to “how to” would appear to be:

    use SymfonyBridgeDoctrineValidatorConstraints as Unique;

    And use it through annotations like in this example:

    /**
    * @UniqueUniqueEntity(fields={“email”})
    */

    [SOURCE: Google's cache of http://www.craftitonline.com/page/7/. It is a snapshot of the page as it appeared on 16 Jul 2011 01:55:33 GMT]

    But then it leads me to this problem:

    + http://groups.google.com/group/symfony2/msg/61182dd6c40ecdba

    Which is where I am stuck for now. :(

    • I found the solution :) [SOURCE:http://groups.google.com/group/symfony2/browse_thread/thread/82f814b141d513b2/e79ce6b93c00d611?lnk=raot%5D

      Since the Constraint is class-centric (and not property or getter-centric) the annotation has to be placed at the top where the class is defined, like so:

      /**
      * MyProjectMyBundleEntityUser
      *
      * @ORMTable(name=”user”,uniqueConstraints={@ORMUniqueConstraint(name=”username_idx”, columns={“username”})})
      * @ORMEntity(repositoryClass=”MyProjectMyBundleRepositoryUserRepository”)
      * @UniqueUniqueEntity(fields={“username”})
      */
      class User
      {

      I was placing the annotation above the $username class property, which is placed in the incorrect context. I found the Google groups thread shown above and suddenly it all made sense and my previous problems went away!

  4. Christopher says:

    Could you please give an example of how to use the UniqueEntityValidator ? I have tried and I always get the Catchable Fatal Error: Argument 1 passed to SymfonyComponentValidatorMappingClassMetadata::addConstraint() must be an instance of SymfonyComponentValidatorConstraint, string given… error.

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 )

Google+ photo

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

Connecting to %s