Как безопасно использовать UniqueEntity (на сайтах с более чем одним одновременным пользователем)

Может ли кто-нибудь умный поделиться с шаблоном дизайна, который они используют, чтобы избежать этой основной и общей проблемы параллелизма в Doctrine \ Symfony?

Сценарий: каждый пользователь должен иметь уникальное имя пользователя.

Неудачное решение:

  • Добавьте ограничение UniqueEntity в объект User.
  • Следуйте шаблону, предложенному в документах Symfony : используйте компонент Form для проверки потенциального нового пользователя. Если это действительно, продолжайте.

Почему это происходит: между проверкой и сохранением пользователя, имя пользователя может быть принято другим пользователем. Если это так, Doctrine генерирует исключительное исключение UniqueConstraintViolationException, когда оно пытается сохранить нового пользователя.

Вот что мой следующий ответ:

  • Он корректно отображает ошибки пользователю, если происходит нарушение ограничения, например, если обработчик обрабатывал его,

  • Это предотвращает «защиту» обновлений базы данных, чтобы разбить логику контроллера (например, с помощью инструкции UPDATE или представления формы с «незащищенными» контроллерами),

  • Это независимое от базы данных решение.

Вот код с пояснениями по комментариям:

<?php // ... use Doctrine\DBAL\Exception\ConstraintViolationException; use Symfony\Component\Form\FormError; use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper; // ... public function indexAction(Request $request) { $task = new Task(); $form = $this->createFormBuilder($task) ->add('name', TextType::class) ->add('save', SubmitType::class, array('label' => 'Create Task')) ->getForm(); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $task = $form->getData(); $em = $this->getDoctrine()->getManager(); $em->persist($task); try { $em->flush(); // Everything went well, do whatever you're supposed to. return $this->redirectToRoute('task_success'); } catch (ConstraintViolationException $e) { // Reopen the entity manager so the validator can do jobs // that needs to be performed with the database (in example: // unique constraint checks) $em = $em->create($em->getConnection(), $em->getConfiguration()); // Revalidate the form to see if the validator knows what // has thrown this constraint violation exception. $violations = $this->get('validator')->validate($form); if (empty($violations)) { // The validator didn't see anything wrong... // It can happens if you have a constraint on your table, // but didn't add a similar validation constraint. // Add an error at the root of the form. $form->add(new FormError('Unexpected error, please retry.')); } else { // Add errors to the form with the ViolationMapper. // The ViolationMapper will links error with its // corresponding field on the form. // So errors are not displayed at the root of the form, // just like if the form was validated natively. $violationMapper = new ViolationMapper(); foreach ($violations as $violation) { $violationMapper->mapViolation($violation, $form); } } } } return $this->render('default/new.html.twig', array( 'form' => $form->createView(), )); } 

Один из способов добиться того, чего вы хотите, – это блокировка с помощью symfony LockHandler .

Вот простой пример, используя шаблон, который вы ссылаетесь на свой вопрос:

 <?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(), )); } 

Примечание. Это не будет работать, если вы запустите приложение на нескольких хостах из документации:

Обработчик блокировки работает только в том случае, если вы используете только один сервер. Если у вас несколько хостов, вы не должны использовать этот помощник.

Вы также можете переопределить EntityManager для создания новой функции, такой как validateAndFlush($entity) которая управляет LockHandler и самим процессом проверки.

Не удалось установить уникальное ограничение на уровне базы данных. Вы также можете проверить документацию Doctrine2 о том, как это сделать:

 /** * @Entity * @Table(name="user", * uniqueConstraints={@UniqueConstraint(name="username_unique", columns={"username"})}, * ) */ class User { //... /** * @var string * @Column(type="string", name="username", nullable=false) */ protected $username; //... } 

Теперь у вас есть уникальное ограничение на уровне базы данных (поэтому одно и то же имя пользователя никогда не может быть вставлено в таблицу пользователя дважды).

Когда вы выполняете операцию вставки, вы получите исключение, если имя пользователя уже существует ( UniqueConstraintViolationException ). Вы можете поймать исключение и вернуть действительный ответ клиенту, в котором вы сообщаете, что это имя пользователя уже использовалось (было в вашей базе данных).

Если я правильно понял вопрос, вы установили очень высокий бар для себя. Очевидно, что ваш слой персидентности не видит будущего. Таким образом, невозможно поддерживать валидатор, который гарантирует, что вставка будет успешной (а не выбрасывает исключение UniqueConstraintViolationException), используя только ваши объекты домена. Где-то вам нужно будет поддерживать дополнительное состояние.

Если вам нужно некоторое постепенное улучшение, вам нужно каким-то образом зарезервировать имя пользователя во время проверки. Это, конечно, достаточно просто – вы просто создаете список где-то, чтобы отслеживать «в полете» имена пользователей, и проверяйте этот список в дополнение к проверке уровня персистентности во время проверки.

Там, где это сложно, следует разработать разумный способ обрезать этот список и выпускать имена пользователей, которые отправляются для проверки, но никогда не использовались при успешной регистрации.

Это деталь реализации, и вам нужно будет подумать о том, как долго хранится имя пользователя.

Простая реализация с моей стороны: поддерживайте таблицу в своей базе данных (имя пользователя, session_id, reserved_at) и регулярно обрабатывайте все строки, где зарезервировано_айт <: datetime.

Вам нужно будет отслеживать session_id, так как вы резервируете имя пользователя для определенного пользователя. Поскольку пользователь еще не создал учетную запись, единственный способ идентифицировать их – через идентификатор сеанса.