Search code examples
phpsymfony-formssymfony-2.7

Symfony2.7 field validates twice, using validation-groups and UniqueEntity


Field gets validated twice, causing 2 DB calls and 2 of the same error message.

I have Subscriber class the main User, Registration class and RegistrationType which embeds SubscriberType as tutorial How to Implement a simple Registration Form suggests. The main username field has constraint of UniqueEntity, and any time a non-unique username is entered the "username already exists" error message pops up twice, checking the log DB call to check for existing username is called twice. I've googled this and have not found a same case as mine, but have found similar cases. I have no idea what to do here.

The database is legacy so I couldn't just switch to FOSUserBundle, and the reason I don't use @ annotations but instead use *.orm.yml and validation.yml

class RegistrationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {

        if(!$options['service']){
            $builder->add('user', new SubscriberType());
            $builder->add('Register', 'submit');    
        } else {
            $subscriber = new Subscriber();                
            $subscriber->setEmail($options['user_information']->getEmail());
            $subscriber->setFirstname($options['user_information']->getFirstname());
            $subscriber->setLastname($options['user_information']->getLastname());
            $subscriber->setUsername($options['user_information']->getEmail());

            switch($options['service']){
                case 'facebook':
                    $subscriber->setFbEmail($options['user_information']->getEmail());
                break;
            }

            $builder->add('user', new SubscriberType($subscriber), array('service' => $options['service'],
                'user_information' => $options['user_information'], 'data'=> $subscriber));
        }

        $builder->add(
            'terms',
            'checkbox',
            array('property_path' => 'termsAccepted')
        );

    }    

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'service' => false,
            'user_information' => false
            ,'validation_groups' => array('Default', 'registration')
        ));
    }

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

the custom 'service' options is to integrate HWIOAuthBundle, whole other story there.

class SubscriberType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        if(!$options['service']){
            $builder->add('username', 'email');
            $builder->add('plainPassword', 'repeated', array(
               'first_name'  => 'password',
               'second_name' => 'confirm',
               'type'        => 'password',
            ));
        } else {


            $builder->add('email', 'text', array('read_only' => true, ));
            $builder->add('firstname', 'text');
            $builder->add('lastname', 'text');
            $builder->add('username', 'hidden');
            $builder->add('fbEmail', 'hidden');
            $builder->add('gplusEmail', 'hidden');
        }
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Subscriber',
            'service' => false,
            'user_information' => false
            ,'validation_groups' => array('Default', 'registration')
        ));
    }

    /* DEPRECATED since 2.7
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Subscriber',
            'service' => false,
            'user_information' => false
        ));
    }
    */

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

Registration.php:

<?php
// src/AppBundle/Form/Model/Registration

namespace AppBundle\Form\Model;

use Symfony\Component\Validator\Constraints as Assert;
use AppBundle\Entity\Subscriber;

class Registration
{
    /**
     * @Assert\Type(type="AppBundle\Entity\Subscriber")
     * @Assert\Valid()
     */
    protected $user;

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

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

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

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

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

Subscriber.php //Entity:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\Common\Collections\ArrayCollection;
//use FOS\UserBundle\Entity\User as BaseUser;
/**
 * Subscriber
 */
class Subscriber implements UserInterface, \Serializable
{
    /**
     * @var integer
     */
    private $id;

    /**
     * @var string
     */
    private $username;

    /**
     * @var string
     */
    private $email;

    /**
     * @var string
     */
    private $gplusEmail;

    /**
     * @var string
     */
    private $fbEmail;

    /**
     * @var string
     */
    private $sha1Password;

    /**
     * @var string
     */
    private $salt;

    /**
     * @var float
     */
    private $unit;

    /**
     * @var string
     */
    private $validate;

    /**
     * @var string
     */
    private $rememberKey;

    /**
     * @var string
     */
    private $firstname = '';

    /**
     * @var string
     */
    private $lastname = '';

    /**
     * @var string
     */
    private $avatar = '';

    /**
     * @var string
     */
    private $countryId = '';

    /**
     * @var string
     */
    private $address;

    /**
     * @var string
     */
    private $phone;

    /**
     * @var string
     */
    private $gender = '';

    /**
     * @var \DateTime
     */
    private $birthdate;

    /**
     * @var \DateTime
     */
    private $lastAttemp;

    /**
     * @var integer
     */
    private $attempCount;

    /**
     * @var \DateTime
     */
    private $lastloginDate;

    /**
     * @var \DateTime
     */
    private $createdAt;

    /**
     * @var \DateTime
     */
    private $updatedAt;

    /**
     * @var \Doctrine\Common\Collections\Collection
     */
    private $groups;


    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $plainPassword;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->groups = new \Doctrine\Common\Collections\ArrayCollection();
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set username
     *
     * @param string $username
     * @return Subscriber
     */
    public function setUsername($username)
    {
        $this->username = $username;

        return $this;
    }

    /**
     * Get username
     *
     * @return string 
     */
    public function getUsername()
    {
        return $this->username;
    }

    /**
     * Set email
     *
     * @param string $email
     * @return Subscriber
     */
    public function setEmail($email)
    {
        $this->email = $email;

        return $this;
    }

    /**
     * Get email
     *
     * @return string 
     */
    public function getEmail()
    {
        return $this->email;
    }

    /**
     * Set gplusEmail
     *
     * @param string $gplusEmail
     * @return Subscriber
     */
    public function setGplusEmail($gplusEmail)
    {
        $this->gplusEmail = $gplusEmail;

        return $this;
    }

    /**
     * Get gplusEmail
     *
     * @return string 
     */
    public function getGplusEmail()
    {
        return $this->gplusEmail;
    }

    /**
     * Set fbEmail
     *
     * @param string $fbEmail
     * @return Subscriber
     */
    public function setFbEmail($fbEmail)
    {
        $this->fbEmail = $fbEmail;

        return $this;
    }

    /**
     * Get fbEmail
     *
     * @return string 
     */
    public function getFbEmail()
    {
        return $this->fbEmail;
    }

    /**
     * Set sha1Password
     *
     * @param string $sha1Password
     * @return Subscriber
     */
    public function setSha1Password($sha1Password)
    {
        $this->sha1Password = $sha1Password;

        return $this;
    }

    /**
     * Get sha1Password
     *
     * @return string 
     */
    public function getSha1Password()
    {
        return $this->sha1Password;
    }

    /**
     * Set salt
     *
     * @param string $salt
     * @return Subscriber
     */
    public function setSalt($salt)
    {
        $this->salt = $salt;

        return $this;
    }

    /**
     * Get salt
     *
     * @return string 
     */
    public function getSalt()
    {
        return $this->salt;
    }

    /**
     * Set unit
     *
     * @param float $unit
     * @return Subscriber
     */
    public function setUnit($unit)
    {
        $this->unit = $unit;

        return $this;
    }

    /**
     * Get unit
     *
     * @return float 
     */
    public function getUnit()
    {
        return $this->unit;
    }

    /**
     * Set validate
     *
     * @param string $validate
     * @return Subscriber
     */
    public function setValidate($validate)
    {
        $this->validate = $validate;

        return $this;
    }

    /**
     * Get validate
     *
     * @return string 
     */
    public function getValidate()
    {
        return $this->validate;
    }

    /**
     * Set rememberKey
     *
     * @param string $rememberKey
     * @return Subscriber
     */
    public function setRememberKey($rememberKey)
    {
        $this->rememberKey = $rememberKey;

        return $this;
    }

    /**
     * Get rememberKey
     *
     * @return string 
     */
    public function getRememberKey()
    {
        return $this->rememberKey;
    }

    /**
     * Set firstname
     *
     * @param string $firstname
     * @return Subscriber
     */
    public function setFirstname($firstname)
    {
        $this->firstname = $firstname;

        return $this;
    }

    /**
     * Get firstname
     *
     * @return string 
     */
    public function getFirstname()
    {
        return $this->firstname;
    }

    /**
     * Set lastname
     *
     * @param string $lastname
     * @return Subscriber
     */
    public function setLastname($lastname)
    {
        $this->lastname = $lastname;

        return $this;
    }

    /**
     * Get lastname
     *
     * @return string 
     */
    public function getLastname()
    {
        return $this->lastname;
    }

    /**
     * Set avatar
     *
     * @param string $avatar
     * @return Subscriber
     */
    public function setAvatar($avatar)
    {
        $this->avatar = $avatar;

        return $this;
    }

    /**
     * Get avatar
     *
     * @return string 
     */
    public function getAvatar()
    {
        return $this->avatar;
    }

    /**
     * Set countryId
     *
     * @param string $countryId
     * @return Subscriber
     */
    public function setCountryId($countryId)
    {
        $this->countryId = $countryId;

        return $this;
    }

    /**
     * Get countryId
     *
     * @return string 
     */
    public function getCountryId()
    {
        return $this->countryId;
    }

    /**
     * Set address
     *
     * @param string $address
     * @return Subscriber
     */
    public function setAddress($address)
    {
        $this->address = $address;

        return $this;
    }

    /**
     * Get address
     *
     * @return string 
     */
    public function getAddress()
    {
        return $this->address;
    }

    /**
     * Set phone
     *
     * @param string $phone
     * @return Subscriber
     */
    public function setPhone($phone)
    {
        $this->phone = $phone;

        return $this;
    }

    /**
     * Get phone
     *
     * @return string 
     */
    public function getPhone()
    {
        return $this->phone;
    }

    /**
     * Set gender
     *
     * @param string $gender
     * @return Subscriber
     */
    public function setGender($gender)
    {
        $this->gender = $gender;

        return $this;
    }

    /**
     * Get gender
     *
     * @return string 
     */
    public function getGender()
    {
        return $this->gender;
    }

    /**
     * Set birthdate
     *
     * @param \DateTime $birthdate
     * @return Subscriber
     */
    public function setBirthdate($birthdate)
    {
        $this->birthdate = $birthdate;

        return $this;
    }

    /**
     * Get birthdate
     *
     * @return \DateTime 
     */
    public function getBirthdate()
    {
        return $this->birthdate;
    }

    /**
     * Set lastAttemp
     *
     * @param \DateTime $lastAttemp
     * @return Subscriber
     */
    public function setLastAttemp($lastAttemp)
    {
        $this->lastAttemp = $lastAttemp;

        return $this;
    }

    /**
     * Get lastAttemp
     *
     * @return \DateTime 
     */
    public function getLastAttemp()
    {
        return $this->lastAttemp;
    }

    /**
     * Set attempCount
     *
     * @param integer $attempCount
     * @return Subscriber
     */
    public function setAttempCount($attempCount)
    {
        $this->attempCount = $attempCount;

        return $this;
    }

    /**
     * Get attempCount
     *
     * @return integer 
     */
    public function getAttempCount()
    {
        return $this->attempCount;
    }

    /**
     * Set lastloginDate
     *
     * @param \DateTime $lastloginDate
     * @return Subscriber
     */
    public function setLastloginDate($lastloginDate)
    {
        $this->lastloginDate = $lastloginDate;

        return $this;
    }

    /**
     * Get lastloginDate
     *
     * @return \DateTime 
     */
    public function getLastloginDate()
    {
        return $this->lastloginDate;
    }

    /**
     * Set createdAt
     *
     * @param \DateTime $createdAt
     * @return Subscriber
     */
    public function setCreatedAt($createdAt)
    {
        $this->createdAt = $createdAt;

        return $this;
    }

    /**
     * Get createdAt
     *
     * @return \DateTime 
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }

    /**
     * Set updatedAt
     *
     * @param \DateTime $updatedAt
     * @return Subscriber
     */
    public function setUpdatedAt($updatedAt)
    {
        $this->updatedAt = $updatedAt;

        return $this;
    }

    /**
     * Get updatedAt
     *
     * @return \DateTime 
     */
    public function getUpdatedAt()
    {
        return $this->updatedAt;
    }

    /**
     * Add groups
     *
     * @param \AppBundle\Entity\SubscriberGroup $groups
     * @return Subscriber
     */
    public function addGroup(\AppBundle\Entity\SubscriberGroup $groups)
    {
        $this->groups[] = $groups;

        return $this;
    }

    /**
     * Remove groups
     *
     * @param \AppBundle\Entity\SubscriberGroup $groups
     */
    public function removeGroup(\AppBundle\Entity\SubscriberGroup $groups)
    {
        $this->groups->removeElement($groups);
    }

    /**
     * Get groups
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getGroups()
    {
        return $this->groups;
    }



    /**
     * Get setPassword
     *
     * @param string 
     */
    public function setPassword($hashed_password)
    {
        return $this->setSha1Password($hashed_password);
    }

    /**
     * Get setPassword
     *
     * @return string 
     */
    public function getPassword()
    {
        return $this->sha1Password;
    }


    ///*
    public function serialize()
    {
        return serialize(array(
            $this->id,
            $this->username,
            $this->sha1Password,
            $this->salt
        ));
    }

    ///*
    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->username,
            $this->sha1Password,
            $this->salt
        ) = unserialize($serialized);
    }
    //*/

    public function getRoles()
    {
        $roles = array();
        //var_dump($this->getGroups());
        foreach($this->getGroups() as $group){
            $roles[] = $group->getRole();
        }
        $roles[] = "ROLE_SUBSCRIBER";
        return array_unique($roles);
    }

    public function eraseCredentials()
    {
    }

    public function getPlainPassword()
    {
        return $this->plainPassword;
    }

    public function setPlainPassword($password)
    {
        $this->plainPassword = $password;
    }

}

And Subscriber.orm.yml as follows:

AppBundle\Entity\Subscriber:
    type: entity
    table: w_subscriber
    repositoryClass: AppBundle\Entity\SubscriberRepository
    id:
        id:
            type: bigint
            nullable: false
            unsigned: false
            comment: ''
            id: true
            generator:
                strategy: IDENTITY
    fields:
        username:
            type: string
            nullable: false
            length: 50
            fixed: false
            comment: ''
        email:
            type: string
            nullable: false
            length: 150
            fixed: false
            comment: ''
        gplusEmail:
            type: string
            nullable: true
            length: 150
            fixed: false
            comment: ''
            column: gplus_email
        fbEmail:
            type: string
            nullable: true
            length: 150
            fixed: false
            comment: ''
            column: fb_email
        sha1Password:
            type: string
            nullable: true
            length: 40
            fixed: false
            comment: ''
            column: sha1_password
        salt:
            type: string
            nullable: true
            length: 32
            fixed: false
            comment: ''
        unit:
            type: float
            nullable: true
            precision: 18
            scale: 2
            comment: ''
            default: '0.00'
        validate:
            type: string
            nullable: true
            length: 10
            fixed: false
            comment: ''
        rememberKey:
            type: string
            nullable: true
            length: 50
            fixed: false
            comment: ''
            column: remember_key
        firstname:
            type: string
            nullable: false
            length: 200
            fixed: false
            comment: ''
        lastname:
            type: string
            nullable: true
            length: 200
            fixed: false
            comment: ''
        avatar:
            type: string
            nullable: false
            length: 200
            fixed: false
            comment: ''
        countryId:
            type: string
            nullable: false
            length: 5
            fixed: false
            comment: ''
            default: MN
            column: country_id
        address:
            type: text
            nullable: true
            length: null
            fixed: false
            comment: ''
        phone:
            type: string
            nullable: true
            length: 50
            fixed: false
            comment: ''
        gender:
            type: string
            nullable: false
            length: 1
            fixed: false
            comment: ''
            default: M
        birthdate:
            type: date
            nullable: true
            comment: ''
        lastAttemp:
            type: datetime
            nullable: true
            comment: ''
            column: last_attemp
        attempCount:
            type: bigint
            nullable: true
            unsigned: false
            comment: ''
            default: '0'
            column: attemp_count
        lastloginDate:
            type: datetime
            nullable: true
            comment: ''
            column: lastlogin_date
        createdAt:
            type: datetime
            nullable: false
            comment: ''
            column: created_at
            gedmo:
                timestampable:
                  on: create
        updatedAt:
            type: datetime
            nullable: false
            comment: ''
            column: updated_at
            gedmo:
                timestampable:
                    on: update
    lifecycleCallbacks: {  }
    manyToMany:
        groups:
          targetEntity: SubscriberGroup
          joinTable:
            name: w_subscriber_credential
            joinColumns:
              subscriber_id:
                referencedColumnName: id
            inverseJoinColumns:
              group_id:
                referencedColumnName: id

and finally validation.yml

# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Subscriber:
    constraints:
      - \Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity:
          fields: [ username ]
          groups: [ registration ]
          message: 'this email is already registered'
    properties:
        email:
            - Email: ~

the controller:

<?php
// src/AppBundle/Controller/SecurityController.php

namespace AppBundle\Controller;

use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use AppBundle\Form\Type\RegistrationType;
use AppBundle\Form\Model\Registration;

class SecurityController extends Controller
{

    /**
     * @Route("/register", name="register")
     */
    public function registerAction()
    {
        $form = $this->createForm(new RegistrationType(), new Registration(), array(
            'action' => $this->generateUrl('register_create'),
        ));

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

    /**
     * @Route("/register/create", name="register_create")
     */    
    public function createAction(Request $request)
    {
        $em = $this->getDoctrine()->getManager();

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

        $form->handleRequest($request);

        if ($form->isValid()) {
            $registration = $form->getData();
            $user = $registration->getUser();

            $user->setEmail($user->getUsername());
            $this->get('my.oauth_aware.user_provider.service')->setUserPassword($user, $user->getPlainPassword());

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

            //return $this->redirectToRoute('homepage');
        }

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

    /**
     * @Route("/login_check", name="login_check")
     */
    public function loginCheckAction()
    {
        // this controller will not be executed,
        // as the route is handled by the Security system
    }
}

note that username is also the email field, I have not posted the template because it is also rather large, and I believe it has nothing to do with validation, but if anyone wants to see it I would gladly post it.

also among the configurations there is config.yml->framework->validation->enable_annotations->true


Solution

  • I'm sorry for wasting everyone's time. The error was in Declaration of the main AppBundle, or actually having declarations of 2 differnet bundle within 1 bundle file hierarchy.

    To override HWIOAuthBundle's templates and controllers, I declared a new bundle within AppBundle, for some reason I thought that a new declaration was required. Silly me.

    So symfony loaded validation.yml twice foreach separate declarations of bundles.

    By moving getParent function into AppBundle declaration, and removing 2nd Bundle class declaration and its initialization from AppKernel solves the problem.