Search code examples
phpvalidationsymfonyunique

Symfony2 UniqueEntity constraint SQL error instead of message


I'm stuck with creating a user registration form with Symfony2. I'm trying to define an Unique constraint on the email attribute of my User class.

Acme\APPBundle\Entity\User.php

namespace Acme\APPBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
* @ORM\Entity
* @ORM\Entity(repositoryClass="Acme\APPBundle\Entity\UserRepository")
* @ORM\Table("users")
* @UniqueEntity(
*       fields={"email"},
*       message="email already used"
* )
*/
class User implements UserInterface, \Serializable
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=255, unique=true)
     * @Assert\NotBlank()
     * @Assert\Email()
     */
    protected $email;


    [...]
}

Acme\APPBundle\Form\Type\UserType.php

namespace Acme\APPBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('email', 'email');
        $builder->add('password', 'repeated', array(
            'first_name' => 'password',
            'second_name' => 'confirm',
            'type' => 'password',
        ));
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Acme\APPBundle\Entity\User',
            'cascade_validation' => true,
        ));
    }

    public function getName()
    {
        return 'user';
    }
}

I've added the constraint following the documentation but I still get an exception like :

An exception occured while executing 'INSERT INTO users ( ... )'
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry

It looks like my message value defined in annotations is ignored and the form validation is bypassed since it should fail before attempting to insert row in database.

Can you tell me what am I doing wrong ?


EDIT :

Following Matteo 'Ingannatore' G.'s advice I've noticed that my form is not properly validated. I forgot to mention that I use a registration class that extends the user form. I've written my code after what is explained in the Symfony Cookbook.

Thus I have :

Acme\APPBundle\Form\Model\Registration.php

namespace Acme\APPBundle\Form\Model;

use Symfony\Component\Validator\Constraints as Assert;

use Acme\APPBundle\Entity\User;

class Registration
{
    /**
     * @Assert\Type(type="Acme\APPBundle\Entity\User")
     */
    protected $user;

    /**
     * @Assert\NotBlank()
     * @Assert\True()
     */
    protected $termsAccepted;

    public function setUser(User $user)
    {
        $this->user = $user;
    }

    public function getUser()
    {
        return $this->user;
    }

    public function getTermsAccepted()
    {
        return $this->termsAccepted;
    }

    public function setTermsAccepted($termsAccepted)
    {
        $this->termsAccepted = (Boolean) $termsAccepted;
    }
}

Acme\APPBundle\Form\Type\RegistrationType.php

namespace Acme\APPBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class RegistrationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('user', new UserType());
        $builder->add('terms', 'checkbox', array('property_path' => 'termsAccepted'));
    }

    public function getName()
    {
        return 'registration';
    }
}

Acme\APPBundle\Controller\AccountController.php

namespace Acme\APPBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

use Acme\AccountBundle\Form\Type\RegistrationType;
use Acme\AccountBundle\Form\Model\Registration;

class AccountController extends Controller
{
    public function registerAction()
    {
        $form = $this->createForm(new RegistrationType(), new Registration());

        return $this->render('AcmeAPPBundle:Account:register.html.twig', array('form' => $form->createView()));
    }



    public function createAction()
    {
        $em = $this->getDoctrine()->getEntityManager();

        $form = $this->createForm(new RegistrationType(), new Registration());

        $form->handleRequest($this->getRequest());

        if ($form->isValid()) { // FIXME !!
            $registration = $form->getData();

            $em->persist($registration->getUser());
            $em->flush();

            return $this->redirect($this->generateUrl('home'));
        }

        return $this->render('AcmeAPPBundle:Account:register.html.twig', array('form' => $form->createView()));
    }

}

I guess the error I get might be caused by the fact that the Registration Form is validated, but the User Form within isn't submitted to validation. Am I wrong ? How can I simply change that behaviour ? I saw there is a cascade_validation option but it seems to be useless here. I think it's strange that Symfony Cookbook provides both guides to create a user provider and create a registration form but does not explain how to get those work along.


Solution

  • I finally found what the acutal problem was. The validation was processed only on the RegistrationType instance but not on the UserType within.

    To make sure that the validation also checks the constraints for the user I added the following code to my RegistrationType class :

    public function setDefaultOptions(Options ResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Acme\APPBundle\Form\Model\Registration',
            'cascade_validation' => true,
        ));
    }
    

    What changes everything is the cascade_validation option that must be set to true for this class while this option is set on the UserType class in the CookBook example.

    Also, don't forget to :

    use Symfony\Component\OptionResolver\OptionsResolverInterface
    

    in the file where you define the setDefaultOptions.