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{ /** * @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;}
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, )); }}
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());}