Bonjour,

Je sollicite votre aide pour la gestion des CollectionType. Pour rendre ma question plus accessible, je vais la translater sur le modèle de la documentation officielle, avec les Tâches (Tasks), et les Tags.

Ce que je souhaite :

  • une tâche disposerait déjà de tags assignés
  • je veux soumettre une nouvelle collection de tags avec une certaine valeur
  • si les tags soumis sont présents, je veux mettre leur valeur à jour
  • si les tags soumis ne sont pas présents, je veux les ajouter tels que soumis
  • les tags existants non soumis doivent rester en place

Mon problème :

  • tous les tags sont systématiquement écrasés par ceux soumis, y-compris avant le handleRequest du form.
  • je ne peux donc même pas faire l'analyse moi-même entre le repository et ce qui vient du formulaire, j'arrive trop tard dans le contrôleur

Au niveau des entités, j'ai une relation ManyToMany avec un attribut supplémentaire nommé value (donc en réalité deux relations OneToMany).

Entity "Task"

class Task
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\OneToMany(mappedBy: 'task', targetEntity: TaskTags::class, orphanRemoval: false, cascade: ['persist'])]
    private Collection $TaskTags;

     /**
     * @return Collection<int, TaskTags>
     */
    public function getTaskTags(): Collection
    {
        return $this->TaskTags;
    }

    public function addTaskTag(TaskTags $TaskTag): self
    {
        // J'ai volontairement supprimé la condition d'équivalence à des fins de test
        $this->TaskTags->add($TaskTag);
        $TaskTag->setTask($this);
        return $this;
    }

    public function removeTaskTag(TaskTags $TaskTag): self
    {
        if ($this->TaskTags->removeElement($TaskTag)) {
            // set the owning side to null (unless already changed)
            if ($TaskTag->getTask() === $this) {
                $TaskTag->setTask(null);
            }
        }
        return $this;
    }
}    

Entity "Tag"

class Tag
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\OneToMany(mappedBy: 'tag', targetEntity: TaskTags::class, orphanRemoval: false)]
    private Collection $TaskTags;

    /**
     * @return Collection<int, TaskTags>
     */
    public function getTaskTags(): Collection
    {
        return $this->TaskTags;
    }

    public function addTaskTag(TaskTags $TaskTag): self
    {
        // J'ai volontairement supprimé la condition d'équivalence à des fins de test
        $this->TaskTags->add($TaskTag);
        $TaskTag->setTag($this);
        return $this;
    }

    public function removeTaskTag(TaskTags $TaskTag): self
    {
        if ($this->TaskTags->removeElement($TaskTag)) {
            // set the owning side to null (unless already changed)
            if ($TaskTag->getTag() === $this) {
                $TaskTag->setTag(null);
            }
        }
        return $this;
    }

}    

Entity "TaskTags"

class TaskTags
{
    #[ORM\Id]
    #[ORM\ManyToOne(inversedBy: 'TaskTags')]
    #[ORM\JoinColumn(nullable: false)]
    private Task $task;

    #[ORM\Id]
    #[ORM\ManyToOne(inversedBy: 'TaskTags')]
    #[ORM\JoinColumn(nullable: false)]
    private Tag $tag;

    // Mon fameux champ additionnel
    #[ORM\Column(nullable: true)]
    private ?int $value = null;

        public function getTask(): ?Task
    {
        return $this->task;
    }

    public function setTask(?Task $task): self
    {
        if(null !== $task) {
            $this->task = $task;
        }
        return $this;
    }

    public function getTag(): ?Tag
    {
        return $this->tag;
    }

    public function setTag(?Tag $tag): self
    {
        if(null !== $tag) {
            $this->tag = $tag;
        }
        return $this;
    }

    public function getValue(): ?string
    {
        return $this->value;
    }

    public function setValue(?string $value): self
    {
        $this->value = $value;

        return $this;
    }

}

FormType "TaskFormType

class TaskFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ...
            ->add('TaskTags', CollectionType::class, [
                'by_reference' => false,
                'entry_type' => TaskTagsFormType::class,
                'entry_options' => ['label' => false],
                'allow_add' => true,
                'allow_delete' => true,
                'prototype' => true,
            ]);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Task::class,
            'csrf_protection' => false
        ]);
    }
}

FormType "TaskTagsFormType

class TaskTagsFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('task')
            ->add('tag')
            ->add('value')
        ;
    }

Contrôleur

 #[Route('/tasks/edit/{id}/tags', name: 'app_edit_task')]
    public function editasktags(Request $request, EntityManagerInterface $em, TaskTagsRepository $TaskTagsRepo): Response
    {
    ...
        // Create an ArrayCollection of the current tags assigned to the task

        $task = $this->getTask();

        // quand on affiche le formulaire (GET), cette collection reflète bien tous les tags assignés à la tâche
        // quand on soumet le formulaire, ça devient immédiatement les tags soumis depuis le formulaire
        $ExistingTaskTags = $TaskTagsRepo->findByTask($task);

        $form = $this->createForm(TaskFormType::class, $task);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // c'est là que j'ai tout essayé ... sauf que comme je ne parviens plus à avoir la collection d'origine, je suis perdu
            $task = $form->getData();

            $SubmittedTaskTags = $userForm->getTaskTags();
            $CalculatedTaskTags = new ArrayCollection();
            foreach ($ExistingTaskTags as $ExistingTaskTag) {
                foreach ($SubmittedTaskTags as $SubmittedTaskTag) {
                    if ($ExistingTaskTag->getTag()->getId() !== $SubmittedTaskTag->getTag()->getId()) {
                        // The existing tag is not the same as submitted, keeping it as it in a new collection
                        $CalculatedTaskTags->add($ExistingTaskTag);
                    } else {
                        // The submitted tag is equal to the one in DB, so adding the submitted one
                        $SubmittedTaskTag->setTask($task);
                        $CalculatedTaskTags->add($SubmittedTaskTag);
                    }
                }
            }
            $em->persist($task);
            $em->flush();
        }
        return $this->render('task/edittasktags.twig.html', [
            'form' => $form,
            'task' => $this->getTask()
        ]);
    }

Mon problème principal réside dans l'incapacité, une fois le formulaire soumis, d'accéder à la collection existante, pour faire un "merge".
Si je fais un dump du repo, les éléments affichés sont ceux soumis dans le formulaire, et c'est tout.

J'ai essayé beaucoup de choses...
Il y en a une que je n'ai pas faîte, et c'est volontaire : passer les éléments existants en "hidden" et les re soumettre.
Je n'aime pas du tout cette pratique, car elle peut être piégeuse avec du multi onglet (on soumettrait des valeurs chargées précédemment, et possiblement modifiées entre temps).

D'avance merci pour votre aide pour ce sujet qui n'est pas simple, j'en conviens !

NB : j'ai volontairement réécrit tout le code en mode "exemple", ça ne correspond pas à la réalité de mes entités.

1 réponse


Solution trouvée.
J'ai ajouté 'mapped' => false dans le FormType

J'ai été en mesure de récupérer les tags soumis avec $SubmittedTags = $form->get('TaskTags')->getData();

Le repository n'est plus écrasé.
En espérant que ça puisse en aider d'autres.