Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to safely use UniqueEntity (on sites with more than one simultaneous user)

Can someone smart can share the design pattern they use to avoid this basic and common concurrency problem in Doctrine\Symfony?

Scenario: Each User must have a unique username.

Failed Solution:

  • Add a UniqueEntity constraint to the User entity.
  • Follow the pattern suggested in Symfony's docs: Use the Form component to validate a potential new User. If it's valid, persist it.

Why It Fails: Between validating and persisting the User, the username may be taken by another User. If so, Doctrine throws a UniqueConstraintViolationException when it tries to persist the newest User.

like image 320
Jeff Clemens Avatar asked Nov 25 '16 00:11

Jeff Clemens


1 Answers

One way to achieve what you want is by locking with the symfony LockHandler.

Here is a simple example, using the pattern you are referring in your question:

<?php

// ...
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Filesystem\LockHandler;
use Symfony\Component\Form\FormError;

public function newAction(Request $request)
{
    $task = new Task();

    $form = $this->createFormBuilder($task)
        ->add('task', TextType::class)
        ->add('dueDate', DateType::class)
        ->add('save', SubmitType::class, array('label' => 'Create Task'))
        ->getForm();

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        // locking here
        $lock = new LockHandler('task_validator.lock');
        $lock->lock();

        // since entity is validated when the form is submitted, you
        // have to call the validator manually
        $validator = $this->get('validator');

        if (empty($validator->validate($task))) {
            $task = $form->getData();
            $em = $this->getDoctrine()->getManager();
            $em->persist($task);
            $em->flush();

            // lock is released by garbage collector
            return $this->redirectToRoute('task_success');
        }

        $form->addError(new FormError('An error occured, please retry'));
        // explicit release here to avoid keeping the Lock too much time.
        $lock->release();

    }

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

NB: This won't work if you run your application on multi hosts, from the documentation:

The lock handler only works if you're using just one server. If you have several hosts, you must not use this helper.

You could also override the EntityManager to create a new function like validateAndFlush($entity) that manage the LockHandler and the validation process itself.

like image 192
Jules Lamur Avatar answered Nov 10 '22 01:11

Jules Lamur