2

I’m updating my user entity. It’s a unique entity defined on it's unique email. Unfortunately, when updating my entity, I’m triggering a validation error due to this email unique validation rule.

I’ve been trying to pass the $user to the form to make sure it considers it as a user update but no luck.

It’s an Ajax form.

Any idea how to work around this please?

User entity

class User implements UserInterface, \Serializable
{
/**
 * @ORM\Id
 * @ORM\Column(type="integer")
 * @ORM\GeneratedValue(strategy="AUTO")
 */
private $id;

/**
 * @var string $email
 * @ORM\Column(type="string", length=254, nullable=false)
 * @Assert\Email()
 * @Assert\NotBlank
 * @AcmeAssert\UniqueEmail
 */
private $email;

/**
 * @ORM\Column(type="string", length=25, nullable=true)
 */
private $username;
// and so on

My controller:

/**
 * @Route("/profile", name="profile")
*/
public function profile()
{
    $user = $this->getUser();
    $formAccount = $this->updateUserAccountForm( $user );

    return $this->render('platform/user/profile.html.twig',
        array(
            'user' => $user,
            'form_account' => $formAccount->createView()
        )
    );
}

/**
 * @Route("/profile/updateAccount", name="updateUserAccount", methods={"POST"})
 */
public function updateUserAccount(Request $request, UserPasswordEncoderInterface $passwordEncode)
{
    if (!$request->isXmlHttpRequest()) {
        return new JsonResponse(array('message' => 'Forbidden'), 400);
    }

    // Build The Form
    $user = $this->getUser();

    $form = $this->updateUserAccountForm($user);
    $form->handleRequest($request);

    if ($form->isValid()) {
        $user_form = $form->getData();

        // Check if the password is = to DB
        $current_password = $passwordEncoder->encodePassword($user, $user_form->getPlainPassword());

        if($user->getPassword() != $current_password){
            return new JsonResponse(['error' => 'wrong password!']);
        }

        // Encode the password (We could also do this via Doctrine listener)
        $password = $passwordEncoder->encodePassword($user_form, $user_form->getPlainPassword());
        $user_form->setPassword($password);

        $entityManager = $this->getDoctrine()->getManager();
        $entityManager->merge($user_form);
        $entityManager->flush();

        $em = $this->getDoctrine()->getManager();
        $em->persist($user_form);
//            $em->flush();

        return new JsonResponse(array('message' => 'Success!'), 200);
    }

    $errors = [];
    foreach ($form->getErrors(true, true) as $formError) {
        $errors[] = $formError->getMessage();
    }
    $errors['userid'] = $user->getId();
    $errors['user'] = $user->getUsername();
    return new JsonResponse($errors);
}

/**
 * Creates a form to update user account.
 *
 * @param User $entity The entity
 *
 * @return \Symfony\Component\Form\FormInterface The form
 */
private function updateUserAccountForm(User $user)
{
    $form = $this->createForm( AccountType::class, $user,
        array(
            'action' => $this->generateUrl('updateUserAccount'),
            'method' => 'POST',
        ));

    return $form;
}

And the AccountType.php

class AccountType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('username', TextType::class)
        ->add('email', EmailType::class, array(
            'required' => true,
            'constraints' => array(
                new NotBlank(),
            )))
        ->add('password', PasswordType::class, array(
            'required' => true
            ))
        ->add('plainPassword', RepeatedType::class, array(
            'type' => PasswordType::class,
            'invalid_message' => 'The new password fields must match.',
            'required' => false,
            'first_options'  => array('label' => 'New Password'),
            'second_options' => array('label' => 'Confirm New Password')
        ))
        ->add('save', SubmitType::class, array('label' => 'Save ->'));
}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        // enable/disable CSRF protection for this form
        'csrf_protection' => true,
        // the name of the hidden HTML field that stores the token
        'csrf_field_name' => '_token',
        // an arbitrary string used to generate the value of the token
        // using a different string for each form improves its security
        'csrf_token_id'   => 'reset_item',
        'data_class' => User::class
    ));
}
}

The ajax call do return a form serialized. The form has been created in the profile function and works well. I’m getting a form error message when calling '$form->isValid()’

I’ve been trying everything to pass the $user in the form type to make Symfony understand it’s based on a pre-existing user. but no luck.

Any help is appreciated thanks!

EDIT: Here is my custom UniqueEmail Validator class:

class UniqueEmailValidator extends ConstraintValidator
{
    /**
     * @var EntityManager
     */
    protected $em;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->em = $entityManager;
    }

    public function validate($value, Constraint $constraint)
    {
        // Do we have a pre registered user in DB from email form landing page?
        $userRepository = $this->em->getRepository(User::class);
        $existingUser = $userRepository->findOneByEmail($value);

        if ($existingUser && $existingUser->getIsActive()) {
            $this->context->buildViolation($constraint->message)
                ->setParameter('{{ string }}', $value)
                ->addViolation();
        }
    }
}
fgamess
  • 1,276
  • 2
  • 13
  • 25
Miles M.
  • 4,089
  • 12
  • 62
  • 108
  • UniqueEmail is a custom constraint validation right? Can you show the code of this constraint validation? Also, the error displayed, please. – fgamess Jun 24 '18 at 08:02
  • 1
    You maybe need to use a UniqueConstraintViolationException in a try/catch , I suggest you to read this : https://stackoverflow.com/questions/3967226/checking-for-duplicate-keys-with-doctrine-2 – BENARD Patrick Jun 24 '18 at 15:23
  • @FranckGamess thanks for your comment, I’ve edit my question posting the UniqueEmailValidator.php file. The messages triggered is the one I set up in UniqueEmail.php in the same validator folder. – Miles M. Jun 25 '18 at 13:00
  • @pbenard Thanks, I understand this scenario is when invoking flush(). Would that work with the form validation ? – Miles M. Jun 25 '18 at 13:01
  • No I don't think, you just have to wrap the flush inside a try\catch, if error is catch, you add Manually an error, with $form->addError, and return the form as this. – BENARD Patrick Jun 25 '18 at 17:16
  • @pbenard thank you! – Miles M. Jun 26 '18 at 11:03

1 Answers1

2

From what I see, when you submit the form the UniqueEmail constraint you set on the email field try to validate the submitted value with the rule you defined. Let's say you want to update the username only, the form will be sent with the current value of the email field stored in the database. And of course, it will trigger a validation error.

Generally, it is better to put a unique constraint on the email column in your user table. So you will be able to update properly your User entity even if you don't modify the email field. And also you won't be able to create a new user in the database having the same email as an existing user. I guess this is what you want.

Doctrine can help you to achieve this.

With @UniqueConstraint on the entity level:

<?php
/**
 * @Entity
 * @Table(name="user_table",uniqueConstraints= {@UniqueConstraint(name="search_idx", columns={"email"})})
 */
class User implements UserInterface, \Serializable
{
}

With @Column on the field level with unique attribute(I prefer this one but depends on the case):

/**
 * @var string $email
 * @ORM\Column(type="string", length=32, unique=true, nullable=false)
 * @Assert\Email()
 * @Assert\NotBlank
 */
private $email;

Just take a look and try to see if it solves your issue.

fgamess
  • 1,276
  • 2
  • 13
  • 25