1

I'm using symfony 4.2 and ReactJS. I have a form with a mail. This mail should be unique. So I have a UniqueEntity included in Entity.

The issue is the following when I try to create an account with the form, it throws me an error 500 : "An error occurred","hydra:description":"An exception occurred while executing \u0027INSERT INTO app_users (id, username, email, is_active, firstname, lastname, api_link_key, facebook_id, facebook_picture_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\u0027 with params XXXXXX : Unique violation: 7 ERROR: duplicate key value violates unique constraint \u0022uniq_c2502824f85e0677\u0022\nDETAIL: Key (username)=(XXXXXX) already exists." So I don't have a message error in the form maybe because of this error 500 ? Or maybe should I set The message error somewhere ?

In my entity when I set the mail field, I set the username field too with the same value. The UniqueEntity forbid to have the same value in two fields in the same row ?

My entity :

* @ORM\Table(name="app_users")
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
* @UniqueEntity("email")
*/
class User implements UserInterface, \Serializable
{
   /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=254, unique=true)
     * @Encrypted
     */
    private $username;

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

   [...]

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email): void
    {
        $this->email = $email;
        $this->username = $email;
    }
}

Thanks for your help

7
  • Before saving the data into the database, are you checking the form/entity validation ? like $form->isValid() or $validator->validate($user) Commented May 3, 2019 at 14:28
  • The front is generated with ReactJS and I'm using api-platform. So I don't have a controller behind to validate data. The NotBlank assert is working because I can't post the form without data Commented May 3, 2019 at 14:35
  • try using a validation groups for the account creation and add the UniqueEntity constraints to the validation groups Commented May 3, 2019 at 14:39
  • 2
    The SQL error is not complaining about your email column, but about the username which you also declared as unique. So what you probably want to do is add an additional constraint that also enforces uniqueness for the username: @UniqueEntity("username") Commented May 3, 2019 at 14:47
  • 1
    hey, have you red the warnings in the basic usage section (below the code example) symfony.com/doc/current/reference/constraints/UniqueEntity.html and have you checked, that those won't cause problems in your case? Commented May 3, 2019 at 16:20

3 Answers 3

5

This happens because you have declared that field username must be unique in the underlying database, but you are not validating in your application that the username property of each entity instance really will be unique. -- That is why the non-unique value is reaching the database layer.

As commented by @Jakumi you need to add a unique entity constraint to for the username property of your entity class. You've already included the email property, so you just need to expand it to include username too.

Change

@UniqueEntity("email")

to

@UniqueEntity("username")
@UniqueEntity("email")

Forgot to mention, make sure you include the UniqueEntity constraint in your class file otherwise it won't work at all:

use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

Finally another problem you may run into is when the entity has a blank username. Because the username property is not nullable it will be assumed to be a blank string. Null values are not considered in unique checks by the database layers because null means "no value defined at all", but blank strings ARE checked because a blank string is a defined value. I suggest you also validate that the username property cannot be blank with @Assert\NotBlank() to avoid this problem.

Sign up to request clarification or add additional context in comments.

4 Comments

As per the manual for the @UniqueEntity fields If you need to require two fields to be individually unique (e.g. a unique email and a unique username), you use two UniqueEntity entries, each with a single field.
Thanks, but I had already edited that in 12 minutes prior to your comment.
I was citing the source of your explanation, since UniqueEntity can accept an array or string for the fields. eg: @UniqueEntity({"email", "username"}) which does not have the same effect.
Hi thanks for your help. I dropped the unique=true on the username field because it's not mandatory. But I still get the error on eMail field now : ERROR: duplicate key value violates unique constraint \u0022uniq_c2502824f85e0677\u0022\nDETAIL: Key (email)=(XXXXXX) already exists.
1

I use Api Platform and symfony @UniqueEntity works fine. You don't need custom constraint.

This annotation works for me.

* @UniqueEntity(
 *     fields={"username"},
 *     errorPath="username",
 *     groups={"ucreate", "screate"},
 *     message="This username is already in use"
 * )

You could define all validation groups you want. For example "ucreate" validation group for POST actions on User Entity, and "screate" group for POST actions on Student entity, which has User entity (embed relation OneToOne).

On embed relations (just en example)
    /**
     * @ORM\OneToOne(targetEntity="App\Entity\User", cascade={"persist", "remove"})
     * @ORM\JoinColumn(nullable=true)
     * @Groups({"student:item:get", "student:write"})
     * @Assert\Valid(groups={"screate"})
     */
    private $user;

Comments

0

Finally, I created a custom validator which retrieve all emails and check that the current one is not already in database.

The constraint :

/**
 * @Annotation
 */
class DuplicateUser extends Constraint
{
    public $message = 'Email already exists.';
}

The constraint validator :

class DuplicateUserValidator extends ConstraintValidator
{
    private $em;
    private $encryptor;

    /**
     * DuplicateUserValidator constructor.
     */
    public function __construct(EntityManagerInterface $em, Encryptor $encryptor)
    {
        $this->em = $em;
        $this->encryptor = $encryptor;
    }

    public function validate($value, Constraint $constraint)
    {
        if (!$constraint instanceof DuplicateUser) {
            throw new UnexpectedTypeException($constraint, DuplicateUser::class);
        }

       // custom constraints should ignore null and empty values to allow
        // other constraints (NotBlank, NotNull, etc.) take care of that
        if (null === $value || '' === $value) {
            return;
        }

        if (!is_string($value)) {
            // throw this exception if your validator cannot handle the passed type so that it can be marked as invalid
            throw new UnexpectedValueException($value, 'string');

            // separate multiple types using pipes
            // throw new UnexpectedValueException($value, 'string|int');
        }

        # Email is encrypted in database, we need to encrypt request's email value before.
        $encryptedEmail = $this->encryptor->encryptWithMarker($value);

        $qb = $this->em->createQueryBuilder();

        $qb->select('u.email')
            ->from(User::class, 'u');

        $arrayEmails = $qb->getQuery()->execute();

        foreach ($arrayEmails as $key => $arrayEmail) {
            foreach ($arrayEmail as $email) {
                if ($encryptedEmail == $email) {
                    $this->context->buildViolation($constraint->message)
                        ->addViolation();
                }
            }
        }
    }
}

And in entity I added @CustomAssert\DuplicateUser (don't forget to add use App\Validator\Constraints as CustomAssert;) :

* @ORM\Table(name="app_users")
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
* @UniqueEntity("email")
*/
class User implements UserInterface, \Serializable
{
    /**
     * @ORM\Column(type="string", length=255, unique=true)
     * @Assert\Email()
     * @Assert\NotBlank()
     * @Encrypted
     * @CustomAssert\DuplicateUser
    */
    private $email;
}

Hope it could helps.

1 Comment

Your implementation for checking duplicates will get very slow once the amount of emails grows in your database. It is more efficient to directly check for the new email by using a where statement.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.