8

Since I am extending the BaseUser, I don't have the property $name in my user entity. How can I add validation to it? Can I just add a property $username to this entity? Would that mess with or override certain functionality or validation or such that is provided natively to a user class?

I rad this question: FOSUserBundle - Validation for username, password or email fields.
But since I am using a bunch of annotation validations, that won't work for me.

Follow-up question - I see that in fosUSerBundle XML file, for validating the user that it has natively a checker for use of a taken username. I don't get that functionality, mine goes straight to a SQL error message if I try to add a user with the same name as an existing user. Is this somehow because I am using annotations which overrides the XML validation file all together?

<?php
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;


class User extends BaseUser implements AdvancedUserInterface
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;  

//TODO: Add regex on $name for a-z, A-Z, 0-9 and _ (underscore) 
//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;   

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 validation 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'),
));
}

Still nothing happens. Straight to the SQL error upon save.

Amit Joshi
  • 15,448
  • 21
  • 77
  • 141
Matt Welander
  • 8,234
  • 24
  • 88
  • 138

5 Answers5

3

I apologize ahead of time. I'm not entirely sure if you want to validate a $name property or the $username property, but I hope this points you in the right direction.

To add a $name property with the regex validation, it should look something like this. Not tested, but this is where I would start:

<?php

use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity
 * @ORM\Table(name="users")
 */
class User extends BaseUser
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string")
     * @Assert\NotBlank(groups={"Registration","Profile"})
     * @Assert\Regex(pattern="/^[a-zA-Z0-9_]+$/", groups={"Registration","Profile"})
     */
    protected $name;

    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    public function getName()
    {
        return $this->name;
    }
}

The unique usernameCanonical and emailCanonical validations are inherited, but they are in validation groups. More information about validation groups here and here.

It seems to me so far that the FOSUserBundle uses the user manager like a repository of sorts, so you will want to use that to interact with the storage layer as much as possible. Here is what that might look like in a controller (again, not tested):

// setting values manually
$userManager = $this->container->get('fos_user.user_manager');
$user = $userManager->createUser();
$user->setName('Joe');
$user->setUsername('joeuser');
$user->setEmail('joe@gmail.com');
$user->setPlainPassword('secret');
$user->addRole('ROLE_EDITOR');

// persist to database
$userManager->updateUser($user);

// inherited validations that check for username and email uniqueness
// using validation group names 'Registration' and 'Profile'
$uniqueEmail = $user->getEmailCanonical();
$uniqueUsername = $user->getUsernameCanonical();

If you are using a form builder, or a form type that is not provided by the bundle, it could look something like this:

// setting values from a form
$userManager = $this->container->get('fos_user.user_manager');
$user = $userManager->createUser();

$form = $this->container->get('form.factory')
    ->createBuilder('form', $user, array('validation_groups' => 'Registration'))
    ->add('username', 'text')
    ->add('email', 'email')
    ->add('name', 'text')
    ->add('plainPassword', 'repeated', array(
        'type' => 'password'
    ))
    ->getForm();

if ($request->isMethod('POST')) {
    $form->handleRequest($request);
    if ($form->isValid()) {
        $userManager->updateUser($user);

        // redirect response
    }
}

return $this->container->get('templating')->renderResponse($templateName, array(
    'form' => $form->createView()
));

I hope this helps. Let me know if I can provide more detail. I would post some of my code here, but my implementation is a bit different.

jrnickell
  • 109
  • 6
  • Now I'm looking at validating the username (althoug a regexp for something else is interesting as well. Is my own answer (which isn't really an answer) to this question shows, I did a little workaround for that. But I can't inherit this said to be "free" functionality of unique username and emai... – Matt Welander Nov 13 '13 at 14:27
  • [Here](https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/config/validation/orm.xml) is the ORM User validation. You will see the `Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity` are set for validation groups 'Registration' and 'Profile', so 'registration' will not work. – jrnickell Nov 14 '13 at 05:43
  • 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
  • I suspect the usernameCanonical field is not set. 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 23:04
3

You should be able to combine constraints defined in both annotations and in PHP (but probably not both those defined in annotations and XML)

<?php

/* ... */

use Symfony\Component\Validator\Mapping\ClassMetadata;

class User extends BaseUser implements AdvancedUserInterface
{
  /* ... */

  public static function loadValidatorMetadata(ClassMetadata $metadata)
  {
    $metadata->addPropertyConstraint(
        'username'
      , new Assert\Regex(array(
            'pattern' => '/^[a-zA-Z0-9_]$/'
        )
    ));
    $metadata->addPropertyConstraint(
        'username'
      , new BizTVAssert\NameExists()
    ));
  }

  /* ... */
}
Peter Bailey
  • 105,256
  • 31
  • 182
  • 206
2

This is not exactly an answer to the question, but a solution for me so far. I added the validator to my form (UserType).

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

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

Implement the callback validation constraint which is documented in the Symfony cookbook: http://symfony.com/doc/current/reference/constraints/Callback.html

Example:

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;

/**
 * @Assert\Callback(methods={"validateName"})
 */
class User extends BaseUser implements AdvancedUserInterface
{
/**
 * @ORM\Id
 * @ORM\Column(type="integer")
 * @ORM\GeneratedValue(strategy="AUTO")
 */
protected $id;  

/**
* @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;   

/**
 * @param ExecutionContextInterface $context
 */
public function validateName(ExecutionContextInterface $context)
{
    // do some validation

    // or add violation
        $context->addViolationAt(
            'name',
            'Name "%name% is not valid',
            array('%name%' => $this->getName()),
            null
        );
}
Pi Wi
  • 1,076
  • 1
  • 11
  • 20
  • Nut to check for other users with same name requires a database connection, I was under the impression such queries can't (or at least shouldn't) be run from within the entity class? – Matt Welander Nov 13 '13 at 14:00
  • In Symfony 2.4 you can set these callback validation in external classes. See the docs: http://symfony.com/doc/master/reference/constraints/Callback.html – Pi Wi Nov 13 '13 at 14:02
  • Ah, misunderstood you. Maybe this is what you searching for? http://knpuniversity.com/screencast/question-answer-day/custom-validation-property-path#creating-a-proper-custom-validation-constraint I don't know a better solution for it – Pi Wi Nov 13 '13 at 14:19
0

For changing the validation on $username, I encourage you to do take the steps describe in FOSUserBundle - Change username validation, it's the easiest way. There's nothing wrong with using both XML and Annotations in a single project. It's actually recommended to not use Annotations, as it keeps the mapping metadata outside of the entities/classes, which is the whole point of the (Data) Mapper pattern.

Another way to go is to not extend FOS\UserBundle\Model\User, but implement FOS\UserBundle\Model\UserInterface. There's a small adjustment you'll have to make to UserProvider and UserManager to make this work, it's described here. Next you'll have to do all the mapping yourself, for which you can freely use any Annotations you want.

As for validating the uniqueness of $username you should note 2 things:

  1. You're not checking if a property is unique, but if the entity is unique (based on a property). So the validator is not placed on a property, but on an entity.
  2. FOSUserBundle translates the $username to $usernameCanonical, and validates if the User entity is unique based on $usernameCanonical. This is actually very nice system, because you can influence the value that is used in the uniqueness check. You can even go further than strtolower (the default), like remove non-word characters, change é to e, etc. So don't really see any value in checking $username itself for uniqueness.
Community
  • 1
  • 1
Jasper N. Brouwer
  • 21,517
  • 4
  • 52
  • 76