php - Form Collection how to avoid creating duplicate record (OneToMany - ManyToOne)
Get the solution ↓↓↓The problem
I have two entities, one calledQuestion
that can be self-referencing, it has an association withQuestionSubQuestions
(it was necessary to add some extra fields likefilter
) so it can have Many questions, but the same questions can be used as children in manyQuestions
. The intention of it is to have a single entity that can have manyChildren
(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 newQuestion
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;
}
FormQuestionType.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 formSubQuestionEmbeddedForm.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());
}
Answer
Solution:
Test this if problem was resolved : (I add title field in Question Entity, change this by your text identification field or remove)
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,
));
}
}
Share solution ↓
Additional Information:
Link To Answer People are also looking for solutions of the problem: filter_sanitize_string deprecated
Didn't find the answer?
Our community is visited by hundreds of web development professionals every day. Ask your question and get a quick answer for free.
Similar questions
Find the answer in similar questions on our website.
Write quick answer
Do you know the answer to this question? Write a quick response to it. With your help, we will make our community stronger.