0

Since the

@UniqueEntity(fields = "username"...

below does absolutely nothing (maybe because username is inherited and not defined explicitly by me in the user class?) I guess I will have to do something as simple as checking for an existing user with that name manually either in the controller or form validation. So I go with form validation.

What I don't know how to do is how to reach the database from the form class, use the queryBuilder from inside the form class manually.

Here's my form class:

namespace BizTV\UserBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormError;

use Doctrine\ORM\EntityRepository;

class editUserType extends AbstractType
{

    function __construct($company)
    {
        $this->company = $company;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {

        $company = $this->company;

        $builder
            ->add('locked', 'checkbox', array('label' => 'Kontot är låst, användaren kan inte logga in '))
            ->add('username', 'text', array('label' => 'Användarnamn '))
        ;

        $builder
            ->add('userGroup', 'entity', array(
                'label' => 'Användargrupp',
                'empty_value' => 'Ingen grupptillhörighet',
                'property' => 'name',
                'class'    => 'BizTV\UserBundle\Entity\UserGroup',
                'query_builder' => function(\Doctrine\ORM\EntityRepository $er) use ($company) {
                    $qb = $er->createQueryBuilder('a');
                    $qb->where('a.company = :company');
                    $qb->setParameters( array('company' => $company) );
                    $qb->orderBy('a.name', 'ASC');

                    return $qb;
                }
            ));         


        $builder
            ->add('email', 'email', array('label' => 'Epost '))
            ->add('plainPassword', 'repeated', array('type' => 'password', 'first_name' => 'Nytt_losenord', 'second_name' => 'Upprepa_losenord',));

        $builder
            ->add('roles', 'choice', array(
                'label' => 'Roller',
                'expanded' => true,
                'multiple' => true,
                'choices'  => array(
                    'ROLE_CONTENT' => 'Innehåll (Användaren kan lägga till, redigera och ta bort innehåll där du nedan beviljar åtkomst)',
                    'ROLE_LAYOUT'  => 'Skärmlayout (Användaren kan skapa ny skärmlayout, redigera befintlig eller ta bort gällande skärmlayout där du nedan beviljar åtkomst)',
                    'ROLE_VIDEO'   => 'Videouppladdning (Användaren har rätt att ladda upp videofiler till företagets mediabibliotek)',
                    'ROLE_ADMIN'   => 'Administratör (Användaren är administratör med fulla rättigheter till allt precis som det konto du nu är inloggad på, var mycket restriktiv med att tilldela denna behörighet).',
                ),
            ))
        ;

        $builder
            ->add('access', 'entity', array(
                'label' => 'Behörigheter',
                'multiple' => true,   // Multiple selection allowed
                'expanded' => true,   // Render as checkboxes
                'property' => 'select_label',
                'class'    => 'BizTV\ContainerManagementBundle\Entity\Container',
                'query_builder' => function(\Doctrine\ORM\EntityRepository $er) use ($company) {
                    $qb = $er->createQueryBuilder('a');
                    $qb->innerJoin('a.containerType', 'ct');
                    $qb->where('a.containerType IN (:containers)', 'a.company = :company');
                    $qb->setParameters( array('containers' => array(1,2,3,4), 'company' => $company) );
                    $qb->orderBy('ct.id', 'ASC');

                    return $qb;
                }
            ));



        $validatorEmail = function(FormEvent $event){
            $form = $event->getForm();
            $myExtraField = $form->get('email')->getData();
            if (empty($myExtraField)) {
              $form['email']->addError(new FormError("Du måste ange en epostadress för användaren"));
            }
        };


        $validatorUsername = function(FormEvent $event){
            $form = $event->getForm();
            $myExtraField = $form->get('username')->getData();
            if (empty($myExtraField)) {
              $form['username']->addError(new FormError("Du måste ange ett namn för användaren"));
            }
            elseif ( preg_match('/^[a-zA-Z0-9_]+$/',$myExtraField) == false ) {
              $form['username']->addError(new FormError("Du får inte använda andra specialtecken än understreck (_)"));
            }            
        };

        $validatorUsernameTaken = function(FormEvent $event){
            $form = $event->getForm();
            $myExtraField = $form->get('username')->getData();

    //TODO: CHECK IN DB FOR USER WITH THAT NAME

            if ($taken) {
              $form['username']->addError(new FormError("Du måste ange ett namn för användaren"));
            }            
        };



        // adding the validator to the FormBuilderInterface
        $builder->addEventListener(FormEvents::POST_BIND, $validatorEmail);
        $builder->addEventListener(FormEvents::POST_BIND, $validatorUsername);
        $builder->addEventListener(FormEvents::POST_BIND, $validatorUsernameTaken);

        //TODO check if username exists 

    }

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

BTW here is my entity:

namespace BizTV\UserBundle\Entity;

use BizTV\UserBundle\Validator\Constraints as BizTVAssert;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

use FOS\UserBundle\Model\User as BaseUser;

use Doctrine\ORM\Mapping as ORM;

use BizTV\BackendBundle\Entity\company as company;

/**
 * @ORM\Entity
 * @ORM\Table(name="fos_user")
 * @UniqueEntity(fields = "username", message = "En användare med det namnet finns redan, försök igen.")
 */
class User extends BaseUser implements AdvancedUserInterface
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;  

//TODO: Add constraint on $name    * @BizTVAssert\NameExists    (and finish coding this constraint)

    /**
    * @var object BizTV\BackendBundle\Entity\company
    *  
    * @ORM\ManyToOne(targetEntity="BizTV\BackendBundle\Entity\company")
    * @ORM\JoinColumn(name="company", referencedColumnName="id", nullable=false)
    */
    protected $company; 

    /**
    * @var object BizTV\UserBundle\Entity\UserGroup
    * @ORM\ManyToOne(targetEntity="BizTV\UserBundle\Entity\UserGroup")
    * @ORM\JoinColumn(name="userGroup", referencedColumnName="id", nullable=true)
    */
    protected $userGroup;   

    /**
     * @ORM\ManyToMany(targetEntity="BizTV\ContainerManagementBundle\Entity\Container", inversedBy="users")
     * @ORM\JoinTable(name="access")
     */
    private $access;

    /**
    * @var object BizTV\ContainerManagementBundle\Entity\Container
    * 
    * This only applies to the BizTV server user accounts or "screen display accounts". Others will have null here. 
    *  
    * @ORM\ManyToOne(targetEntity="BizTV\ContainerManagementBundle\Entity\Container")
    * @ORM\JoinColumn(name="screen", referencedColumnName="id", nullable=true)
    */
    protected $screen;  

    /**
     * @ORM\Column(type="boolean", nullable=true)
     */
    protected $isServer;


    public function __construct()
    {
        parent::__construct();

        $this->access = new \Doctrine\Common\Collections\ArrayCollection();

    }

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

    /**
     * Set company
     *
     * @param BizTV\BackendBundle\Entity\company $company
     */
    public function setCompany(\BizTV\BackendBundle\Entity\company $company)
    {
        $this->company = $company;
    }

    /**
     * Get company
     *
     * @return BizTV\BackendBundle\Entity\company 
     */
    public function getCompany()
    {
        return $this->company;
    }

    /**
     * Add access
     *
     * @param BizTV\ContainerManagementBundle\Entity\Container $access
     */
    public function addContainer(\BizTV\ContainerManagementBundle\Entity\Container $access)
    {
        $this->access[] = $access;
    }

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


    /**
     * Set screen
     *
     * @param BizTV\ContainerManagementBundle\Entity\Container $screen
     */
    public function setScreen(\BizTV\ContainerManagementBundle\Entity\Container $screen)
    {
        $this->screen = $screen;
    }

    /**
     * Get screen
     *
     * @return BizTV\ContainerManagementBundle\Entity\Container 
     */
    public function getScreen()
    {
        return $this->screen;
    }

    /**
     * Set isServer
     *
     * @param boolean $isServer
     */
    public function setIsServer($isServer)
    {
        $this->isServer = $isServer;
    }

    /**
     * Get isServer
     *
     * @return boolean 
     */
    public function getIsServer()
    {
        return $this->isServer;
    }

    /**
     * Set userGroup
     *
     * @param BizTV\UserBundle\Entity\UserGroup $userGroup
     */
    public function setUserGroup(\BizTV\UserBundle\Entity\UserGroup $userGroup = null)
    {
        $this->userGroup = $userGroup;
    }

    /**
     * Get userGroup
     *
     * @return BizTV\UserBundle\Entity\UserGroup 
     */
    public function getUserGroup()
    {
        return $this->userGroup;
    }

    //Below should be part of base user class but doesn't work so I implement it manually.

    /**
     * Get lock status
     *
     * @return boolean 
     */
    public function getLocked()
    {
        return $this->locked;
    }    



}

----- UPDATE -----

My controller's update User method:

public function createUserAction()
{
    $tempCompany = $this->container->get('security.context')->getToken()->getUser()->getCompany()->getId(); 

    $entity  = new User();
    $request = $this->getRequest();
    $form    = $this->createForm(new newUserType($tempCompany), $entity);
    $form->bind($request);

    if ($form->isValid()) {             

        $em = $this->getDoctrine()->getManager();

        $userManager = $this->container->get('fos_user.user_manager');
        $user = $userManager->createUser();

        $encoder = $this->container->get('security.encoder_factory')->getEncoder($user); //get encoder for hashing pwd later

        //get company
        $currentCompany = $this->container->get('security.context')->getToken()->getUser()->getCompany();

        //Set company
        $entity->setCompany( $currentCompany );

        $tempUsername = $entity->getUsername();
        $entity->setUsername($currentCompany->getCompanyName() . "-" . $tempUsername);

        $entity->setConfirmationToken(null);
        $entity->setEnabled(true);
        $tempPassword = $encoder->encodePassword($entity->getPassword(), $entity->getSalt()); 
        $entity->setPassword($tempPassword);

        $em->persist($entity);
        $em->flush();

        $helper = $this->get('biztv.helper.globalHelper');
        $helper->log('success', 'Användare <strong>'.$entity->getUsername().'</strong> har skapats.');

        return $this->redirect($this->generateUrl('UserManagement'));

    }

    return $this->render('BizTVUserBundle:Default:newUser.html.twig', array(
        'entity' => $entity,
        'form'   => $form->createView()
    ));
}   

So am I doing this all wrong? I am in fact using the userManager as you suggest, but only for encoding the password. Should I be persisting the $user created with $userManager instead of persisting my own $entity, would that let me take advantage of the unique checking etc.?

------------ UPDATE ---------------

@jmickell (posting answer here since there is no code formatting in the comments, thank you for your time. I do however not have a solution yet...)

I altered the code according to this

        $em = $this->getDoctrine()->getManager();

        $userManager = $this->container->get('fos_user.user_manager');
        $user = $userManager->createUser();

        $encoder = $this->container->get('security.encoder_factory')->getEncoder($user); //get encoder for hashing pwd later

        //get company
        $currentCompany = $this->container->get('security.context')->getToken()->getUser()->getCompany();

        //Set company
        $entity->setCompany( $currentCompany );

        $tempUsername = $entity->getUsername();
        $entity->setUsername($currentCompany->getCompanyName() . "-" . $tempUsername);

        $entity->setConfirmationToken(null);
        $entity->setEnabled(true);

        $tempPassword = $encoder->encodePassword($entity->getPassword(), $entity->getSalt()); 
        $entity->setPassword($tempPassword);

        $userManager->updateUser($entity);

And it saves the user like you said it would without the flush, there is however no validation on the username/password. And I guess that is something that must happen above this, in the form isValid? or inside the form class itself?

If you take a look at my form class (very top of the question) am I missing something mandatory for those validators of usernameCanonical and emailCanonical to be function?

Also, If I replace this

        $tempPassword = $encoder->encodePassword($entity->getPassword(), $entity->getSalt()); 
        $entity->setPassword($tempPassword);

with this (as I understood you suggested)

$entity->setPlainPassword( $entity->getPassword() );

I end up with a SQL error telling me password field can't be empty...

I have also tried adding the validation groups to the form like this, but no change in behaviour at all

So I have looked here (symfony2 using validation groups in form) and tried that solution by adding this to the controller when building the form, nothing happens (still jumps straight to the INSERT INTO SQL error)

    $entity  = new User();
    $request = $this->getRequest();
    $form    = $this->createForm(new newUserType($tempCompany), $entity, array('validation_groups'=>'registration'));
    $form->bind($request);

I have also tried to set this validatino group "registration group" (that is supposed to give you unique user & email validation "for free") this way, in the form class:

use Symfony\Component\OptionsResolver\OptionsResolverInterface;

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
    'validation_groups' => array('registration'),
));
}
Community
  • 1
  • 1
Matt Welander
  • 8,234
  • 24
  • 88
  • 138

3 Answers3

1

If you use the FOSUserBundle UserManager to update your user entity, you do not need to worry about uniqueness in your username field. The base User has usernameCanonical and emailCanonical fields that are validated for uniqueness.

An event listener is used behind the scenes to update the canonical fields before persisting the User (when using the Doctrine implementation).

"To save a user object, you can use the updateUser method of the user manager. This method will update the encoded password and the canonical fields and then persist the changes."

Updating a User object

You get unique username and email fields for free!

jrnickell
  • 109
  • 6
  • So am I doing this all wrong? I am in fact using the userManager as you suggest, but only for encoding the password. Should I be persisting the $user created with $userManager instead of persisting my own $entity, would that let me take advantage of the unique checking etc.? (see added sample code above) – Matt Welander Nov 09 '13 at 18:25
  • That is correct. If you call `$userManager->updateUser($user);` to persist your users, the user manager will convert the username and email address to all lower-case. The user manager stores the 'canonical' or all lower-case versions in different properties, which have unique constraints built-in. Also, the forms that come with the bundle include the proper validations, but be sure to include validations for any forms you create. The unique properties are `$usernameCanonical` and `$emailCanonical`. – jrnickell Nov 11 '13 at 23:24
  • In your code above, you already have reference to the user manager, so replace `$em->persist($entity);` and `$em->flush();` with `$userManager->updateUser($entity);`. No need to call flush either, it is done for you. You can use `$userManager->updateUser($entity, false);` if you want to avoid the flush until later. – jrnickell Nov 11 '13 at 23:33
  • You do not need to encode the password the way you are doing. The user manager provides some conveniences to help you there as well. Instead, call `$entity->setPlainPassword($rawPassword);` and when you update, it will encode the password for you. Check out this code [UserManager](https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Doctrine/UserManager.php) which extends this [UserManager](https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Model/UserManager.php). – jrnickell Nov 11 '13 at 23:34
  • added answer to these in my original question above – Matt Welander Nov 13 '13 at 15:08
  • There are a few things that I would change in what you are doing. You are calling `$entity->getPassword();` but that should be null, since you do not ever set the password field. Your form is using plainPassword (which it should). You do not need to do anything to set the password, or encode it, since you have plainPassword in your form class. – jrnickell Nov 14 '13 at 04:59
  • You are on the right track by adding the validation_groups to either the setDefaultOptions method in your form class, or during construction. I think the only reason the username and email validations are not being picked up is that the validation group used is 'Registration' with a capital 'R'. Try that out. Everything else looks good from what I can tell. There are a few lines of code, like `$user = $userManager->createUser();` that seem to be unused. Also, your form class is called editUserType, while your controller code has newUserType... is that correct? – jrnickell Nov 14 '13 at 05:03
  • I tried capital R and small r, makes no difference =( Still no validation on those properties. You are right about newUserType and editUserType, I have both and I should have pasted the code from newUSerType in here, but they are essentially the same, only difference is that newUser requires a password, and editUser has an option to lock the user while newUser doesnt. – Matt Welander Nov 14 '13 at 20:15
  • Try `$userManager->updateUser($entity, false); var_dump($entity); exit();` to see what your entity values are. I'm not sure what else to try, since you have a lot of different code trying to do the same thing (like the validator event listeners in your form). I typically solve these kinds of problems by looking at the data in the object to make sure everything is set correctly. Specifically, usernameCanonical, emailCanonical, plainPassword, password, etc... – jrnickell Nov 14 '13 at 23:01
  • hehe, that's a maze without comparison dude... a search for 'usernameCanonical' in that dump gives 6428 hits, a search for '@' giver 770 hits... – Matt Welander Nov 15 '13 at 09:44
  • also - does it really matter how I update the user, the validation should kick in at this line: if ($form->isValid()) { – Matt Welander Nov 15 '13 at 09:46
  • You must be getting the other users nested within groups. You know, you make a good point about `$form->isValid();`. Try calling `$userManager->updateCanonicalFields($entity);` **before** your validations. I wanted to find out if the usernameCanonical field was set (otherwise the validation will not work). If you call updateCanonicalFields manually, then validate, I bet it fixes your problem. – jrnickell Nov 15 '13 at 22:52
  • nope, no reaction from my app whatsoever on implementing that... I'm just gonna go ahead and code a database call in the controller to manually check for the username in DB. There seems to be no other way around it. – Matt Welander Nov 20 '13 at 21:11
0

first add this in your entity:

use Symfony\Bridge\Doctrine\Validator\Constraints as DoctrineAssert;

and then change your code to

@DoctrineAssert\UniqueEntity(fields = "username", message = "En användare med det namnet finns redan, försök igen.")

instead of

@UniqueEntity(fields = "username", message = "En användare med det namnet finns redan, försök igen.")
Zakaria
  • 983
  • 15
  • 24
0

I guess my problem is hidden in my code elsewhere since no suggestion works... guess it is too localized then.

Anyways, I ended up having to solve it this way, "manually" without using the built-in functions for validation. Very annoying but it works. Before the ->isValid() call I added these...

           //check for users with same name or email
            $nameOccupied = $this->checkNameOccupied($entity);
            if ($nameOccupied==1) {                        
                    $error = new FormError("Det finns redan en användare med det namnet.");
                    $form->get('username')->addError($error);        
            }

            $emailOccupied = $this->checkEmailOccupied($entity);
            if ($emailOccupied==1) {                        
                    $error = new FormError("Det finns redan en användare med den epostadressen.");
                    $form->get('email')->addError($error);        
            }

    if ($form->isValid()) {     

Here is the actual checker..

   /* check whether name of user already is in use */
    private function checkNameOccupied($entity) {

            $name = $entity->getUsername();

            //get company
            $currentCompany = $this->container->get('security.context')->getToken()->getUser()->getCompany();

            $tempUsername = $entity->getUsername();
            $needle = $currentCompany->getCompanyName() . "-" . $tempUsername;

            //look for entity with same name inside container
            $repository = $this->getDoctrine()->getRepository('BizTVUserBundle:User');

            $query = $repository->createQueryBuilder('c')
                    ->where('c.username = :name')
                    ->setParameters(array('name' => $needle))
                    ->getQuery();


            $match = $query->getResult();

            if(isset($match[0])) {
                    //make sure we aren't validating against itself
                    if ( $entity->getId() && $entity->getId() == $match[0]->getId() ) {
                            return false;
                    }
                    else {
                            return true;
                    }                        
            }
            else {
                    return false;
            }


    }
Matt Welander
  • 8,234
  • 24
  • 88
  • 138