Form Collection how to avoid creating duplicate record (OneToMany – ManyToOne)

The problem

I have two entities, one called Question that can be self-referencing, it has an association with QuestionSubQuestions (it was necessary to add some extra fields like filter) so it can have Many questions, but the same questions can be used as children in many Questions. The intention of it is to have a single entity that can have many Children (Questions) and re-using the existing ones.

The problem I’m facing is that when I add an existing Question as a child, it creates a new Question record in the database instead of using the existing record for the association.

I have a web interface where the user can pick form a list of existing questions and add it as a child to the main one. The form POST all the info (including the id of the entity) and doctrine process it by itself.

Everything is saved and removed correctly when adding non-existing questions (new ones) but when picking an existing one makes the mentioned error. But this doesn’t happen when the Question gets updated, doctrine persists the existing relations correctly and the create duplicated records don’t appear.

Also, the controller doesn’t contain anything special, but when dumping the form’s data I can see that the added question doesn’t have the __isInitialized__ property, so I can guess doctrine doesn’t really know that that entity already exist. You can see in the dump (see code section) that child with index 0 has the parameter and the one with index 1 doesn’t.

The question

So, how I can fix this? Maybe is there a way to check if the entity exists while processing the form data and attach the entity again to the EntityManager? I know I can make a Listener for that, but I don’t know if is a good practice.

Any help will be apreciated.

The actual code

Form data dump:

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
{
    /**
     * @ORMId
     * @ORMGeneratedValue
     * @ORMColumn(type="integer")
     */
    private $id;

    ...

    /**
     * @var ArrayCollection
     * @ORMOneToMany(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
{
    /**
     * @ORMId
     * @ORMManyToOne(targetEntity="Question", inversedBy="children", cascade={"persist"})
     * @ORMJoinColumn(nullable=false)
     */
    private $question;

    /**
     * @ORMId
     * @ORMManyToOne(targetEntity="Question", cascade={"persist"})
     * @ORMJoinColumn(nullable=false)
     */
    private $subQuestion;

    /**
     * @ORMId
     * @ORMColumn(type="string")
     * @ORMJoinColumn(nullable=false)
     */
    private $filter;
}

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

Embedded form 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,
        ));
    }
}

Edit controller

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

Source: Symfony Questions

Was this helpful?

0 / 0

Leave a Reply 0

Your email address will not be published. Required fields are marked *