Коллекция форм, как избежать создания повторяющихся записей (OneToMany - ManyToOne)

Проблема

У меня есть две сущности, одна из которых называется Question, которая может ссылаться на себя, она связана с QuestionSubQuestions (необходимо было добавить некоторые дополнительные поля, такие как filter), поэтому у нее может быть много вопросов, но те же вопросы могут использоваться как дочерние в много Questions. Цель этого состоит в том, чтобы иметь единую сущность, которая может иметь много Children (Вопросов) и повторно использовать существующие.

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

У меня есть веб-интерфейс, в котором пользователь может выбрать список существующих вопросов и добавить его в качестве дочернего к основному. Форма POST всю информацию (включая идентификатор объекта) и доктрина обрабатывают ее сами по себе.

При добавлении несуществующих вопросов (новых) все сохраняется и удаляется корректно, но при выборе существующего выдает указанную ошибку. Но этого не происходит, когда вопрос обновляется, доктрина корректно сохраняет существующие отношения, а создание дублирующихся записей не появляется.

Кроме того, контроллер не содержит ничего особенного, но при выводе данных формы я вижу, что добавленный вопрос не имеет свойства __isInitialized__, поэтому я могу предположить, что доктрина на самом деле не знает, что эта сущность уже существует. Вы можете видеть в дампе (см. раздел кода), что дочерний элемент с индексом 0 имеет параметр, а дочерний элемент с индексом 1 — нет.

Вопрос

Итак, как я могу это исправить? Может быть, есть способ проверить, существует ли объект при обработке данных формы, и снова прикрепить объект к EntityManager? Я знаю, что могу сделать для этого Listener, но я не знаю, является ли это хорошей практикой.

Любая помощь будет оценена по достоинству.

Действительный код

Дамп данных формы:

Question^ {#1535 ▼
  -id: 56
  -question: "TestB1"
  -children: PersistentCollection^ {#1562 ▼
    -owner: Question^ {#1535}
    -association: array:15 [ …15]
    -em: EntityManager^ {#238 …11}
    -isDirty: true
    #collection: ArrayCollection^ {#1563 ▼
      -elements: array:3 [▼
        0 => QuestionSubQuestion^ {#1559 ▼
          -question: Question^ {#1535}
          -subQuestion: Question^ {#1592 ▼
            +__isInitialized__: true
            -id: "57"
            -question: "P-1"
          }
          -filter: "affirmative"
        }
        1 => QuestionSubQuestion^ {#2858 ▼
          -question: Question^ {#1535}
          -subQuestion: Question^ {#2863 ▼
            -id: "57"
            -question: "P-1"
          }
          -filter: "negative"
        }
      ]
    }
    #initialized: true
  }
}

Question.php

class Question
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    ...

    /**
     * @var ArrayCollection
     * @ORM\OneToMany(targetEntity="QuestionSubQuestion", mappedBy="question", fetch="EAGER" ,cascade={"persist"}, orphanRemoval=true)
     */
    private $children;

    ...

    /**
     * @param QuestionSubQuestion $children
     */
    public function addChild(QuestionSubQuestion $children): void
    {
        if ($this->children->contains($children)) {
            return;
        }

        $children->setQuestion($this);
        $this->children->add($children);
    }

    /**
     * @param mixed $children
     */
    public function removeChild(QuestionSubQuestion $children): void
    {
        if (!$this->children->contains($children)) {
            return;
        }

        $this->children->removeElement($children);
        // needed to update the owning side of the relationship!
        $children->setSubQuestion(null);
    }

}

QuestionSubQuestion.php

class QuestionSubQuestion
{
    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Question", inversedBy="children", cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $question;

    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Question", cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $subQuestion;

    /**
     * @ORM\Id
     * @ORM\Column(type="string")
     * @ORM\JoinColumn(nullable=false)
     */
    private $filter;
}

Форма QuestionType.php

class QuestionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('question')
            ->add('children', CollectionType::class, [
                'entry_type' => SubQuestionEmbeddedForm::class,
                'allow_add' => true,
                'allow_delete' => true,
                'label' => false,
                'by_reference' => false,
                'prototype_name' => '__subQuestion__',
            ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => Question::class,
        ));
    }
}

Встроенная форма SubQuestionEmbeddedForm.php

class SubQuestionEmbeddedForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('subQuestion', SubQuestionType::class)
            ->add('filter', HiddenType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => QuestionSubQuestion::class,
        ));
    }
}

SubQuestionType.php

class SubQuestionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('id', HiddenType::class, [
                'required' => false,
            ])->add('question', TextType::class, [
                'label' => false,
            ])
            ->add('country', HiddenType::class)
            ->add('category', HiddenType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => Question::class,
        ));
    }
}

Редактировать контроллер

$question = $questionRepository->find($questionId);

$form = $this->createForm(QuestionType::class, $question);
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
    $question = $form->getData();

    $questionRepository->save($question);

    return $this->redirect($request->getUri());
}

person Williams A.    schedule 21.04.2020    source источник
comment
Я не уверен, что понял, но я думаю, вы хотели бы создать дерево вопросов. Вам не нужен класс SubQuestion, и дети Question тоже являются Question. Я не ошибаюсь ?   -  person threeside    schedule 21.04.2020
comment
@threeside Да, SubQuestion не является существующим классом, но это тот же класс Question, у которого может быть много потомков Question. Причиной этого является добавление некоторых дополнительных полей (не только идентификаторов) в реляционную модель. Поскольку вы не можете создать ManyToMany с дополнительными полями, мне пришлось сделать OneToMany (Вопрос->Вопрос-ПодВопрос) и ManyToOne (Вопрос-ПодВопрос->Вопрос). Эта часть всей формы работает правильно, проблема заключается в том, что при добавлении существующего вопроса в качестве дочернего я хочу, чтобы доктрина выбрала идентификатор опубликованного вопроса и связала два объекта, не создавая новую запись вопроса.   -  person Williams A.    schedule 21.04.2020
comment
Хорошо, сейчас я понимаю. Вы не показываете свой SubQuestionType. Есть ли у вас возможность добавить новый вопрос в качестве дочернего непосредственно в SubQuestionType или просто использовать существующий вопрос?   -  person threeside    schedule 21.04.2020
comment
@threeside Извините, я только что добавил тип формы. Да, в интерфейсе у меня есть кнопка, использующая HTML-прототип поля children, поэтому я могу добавить новый дочерний элемент с новыми уникальными данными. Но у меня также есть список выбора, созданный с помощью twig, где я выбираю идентификатор нужного вопроса и вручную с помощью Javascript добавляю вопрос в коллекцию, используя прототип с другой кнопки, а затем передаю данные на входы, даже идентификатор . Затем я блокирую поля, чтобы избежать манипулирования данными, поскольку они должны содержать те же данные, что и исходный вопрос.   -  person Williams A.    schedule 21.04.2020


Ответы (1)


Проверьте это, если проблема была решена: (я добавляю поле заголовка в объект вопроса, изменяю его с помощью поля идентификации текста или удаляю)

class QuestionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title', TextType::class)

            ->add('children', CollectionType::class, [
                'entry_type' => QuestionSubQuestionType::class,
                'allow_add' => true,
                'allow_delete' => true,
                'label' => false,
                'by_reference' => false,
                'prototype'    => true,
                'prototype_name' => '__subQuestion__',
            ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => Question::class,
        ));
    }
}






class QuestionSubQuestionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('subQuestion', EntityType::class, [
                'label' => false,
                    'class'    => 'YourBundle:Question',
                    'choice_label' => 'title',

                    'multiple' => false,
                    'expanded'  => false,
                    /* use query builder to customize choices
                    'query_builder' => function (MaterialRepository $er) {
                        return $er->getQbOrderBy('m.id', 'DESC');
                     },*/

                ))
             ->add('filter', HiddenType::class)

            /* uncomment if these fields are in SubQuestion Entity
            ->add('country', HiddenType::class)
            ->add('category', HiddenType::class)
            */
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => QuestionSubQuestion::class,
        ));
    }
}
person threeside    schedule 21.04.2020
comment
Я думаю, что лучше использовать только существующий результат в вашем subQuestionFormType. В вашем случае я использую запрос $ajax, чтобы добавить новый вопрос и добавить его в список в subQuestionType. - person threeside; 21.04.2020
comment
Спасибо за ваше время, я попробую ваше решение. - person Williams A.; 22.04.2020