2016-11-25 2 views
8

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

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

Ошибка Решение:

  • Добавить UniqueEntity ограничение к объекту пользователя.
  • Следуйте за pattern suggested in Symfony's docs: Используйте компонент Form для проверки потенциального нового пользователя. Если это действительно, продолжайте.

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

+0

Вы можете создать его в начале процесса только с именем пользователя, когда это подтверждено, поэтому он не может быть принято кем-то другим? –

+0

@ Антония Томпсон, спасибо за ваш вклад. Минимизация времени между проверкой и сохранением уменьшит частоту. Я надеюсь, что его можно устранить. –

+4

То, что я имею в виду, это то, что он может быть частью проверки, поэтому, если Doctrine выбрасывает UniqueConstraintViolationException, тогда имя пользователя является недопустимым, и вы делаете выбор другим –

ответ

2

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

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

  • Это предотвращает обновления базы данных, которые не «защищенный», чтобы разбить логику контроллера (например, с помощью инструкции 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(), 
    )); 
} 
+0

«Повторно открыть объект менеджер, чтобы валидатор мог выполнять задания «Не делайте этого. Когда менеджер объектов закрыт, его нельзя использовать снова. Это может привести к недействительным состояниям объектов или объектов, которые сохраняются снова. Созданный диспетчер объектов представляет собой новый экземпляр с новой единицей работы. –

1

Один из способов добиться того, чего вы хотите, путем блокировки с помощью 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(), 
    )); 
} 

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

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

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

+0

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

+0

Чтобы уловить ошибки от других * незащищенных контроллеров *, вы можете добавить try-catch в 'ConstraintViolationException' во время' flush'. Добавьте ошибку в форму (если исключение поймано) с помощью '$ form-> addError (Symfony/Component/Form/FormError (« Произошла ошибка, повторите попытку »))'. NB: Это * multi hosts proof * :) –

0

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

/** 
* @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). Вы можете поймать исключение и вернуть действительный ответ клиенту, в котором вы сообщаете, что это имя пользователя уже использовалось (было в вашей базе данных).

+0

У него уже есть уникальное ограничение на его столбец имени пользователя. –

+0

@ Kadriles благодарю вас за ваш комментарий. Я действительно не вижу в этом проблемы, просто поймаю исключение и верну правильный ответ клиенту ... – Wilt

+0

С моей точки зрения, он хочет обрабатывать уникальные нарушения ограничений (или предотвращать их в каждом случае), чтобы отобразить сообщение об ошибке, подобное валидаторным. Таким образом, клиенты никогда не видят неожиданных ошибок (например, страницы с ошибкой 500). Ваш ответ - один из способов сделать это (исключение доктрины try-catch). –

0

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

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

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

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

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

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

+0

Разве это не так, как ответ Вангосо? –

+0

Аналогичные, но не идентичные. Vangoso предлагает создать класс сущности. Я бы не стал рассматривать вопрос о резервировании имен для кратковременной части основной проблемной области и поэтому избегать создания объекта. Вместо этого я бы просто использовал простую таблицу или даже некоторый уровень кеша (например, если у вас уже есть экземпляр redis в вашем стеке, вы можете изобрести способ использовать автоматическое выключение ключа там) – timdev