formsentity-relationshipsymfony5mappedby

Symfony 5: ManyToMany Relationship Not Persisting When Added from MappedBy Side Without 'by_reference' => false


Context:

I'm working on a Symfony 5 application with entities InventaireActif and TypeActif having a ManyToMany relationship. An issue arises when managing this relationship through a Symfony form.

Problem:

In my form for TypeActif, when I add InventaireActif entities, they are not persisted to the database unless I set the 'by_reference' => false option in the form builder. I want to achieve the correct behavior without relying on this option.

Entity Configuration:

InventaireActif.php:

/**
 * @ORM\ManyToMany(targetEntity=TypeActif::class, inversedBy="lesActifs")
 */
private $lesTypesActifs;

public function __construct()
{
    $this->lesTypesActifs = new ArrayCollection();
    // ... other initializations ...
}

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

    public function addLesTypesActif(TypeActif $lesTypesActif): self
    {
        if (!$this->lesTypesActifs->contains($lesTypesActif)) {
            $this->lesTypesActifs[] = $lesTypesActif;
        }

        return $this;
    }

    public function removeLesTypesActif(TypeActif $lesTypesActif): self
    {
        $this->lesTypesActifs->removeElement($lesTypesActif);

        return $this;
    }

TypeActif.php:

/**
 * @ORM\ManyToMany(targetEntity=InventaireActif::class, mappedBy="lesTypesActifs")
 */
private $lesActifs;

public function __construct()
{
    $this->lesActifs = new ArrayCollection();
    // ... other initializations ...
}

public function getLesActifs(): Collection
{
    return $this->lesActifs;
}

public function addLesActif(InventaireActif $lesActif): self
{
    if (!$this->lesActifs->contains($lesActif)) {
        $this->lesActifs[] = $lesActif;
        $lesActif->addLesTypesActif($this);
    }
    return $this;
}

public function removeLesActif(InventaireActif $lesActif): self
{
    if ($this->lesActifs->removeElement($lesActif)) {
        $lesActif->removeLesTypesActif($this);
    }
    return $this;
}

Form Configuration in TypeActifType.php:

public function buildForm(FormBuilderInterface $builder, array $options): void
{
    $builder
        // ... other fields ...
        ->add('lesActifs', EntityType::class, [
            'required' => false,
            'class' => InventaireActif::class,
            'choice_label' => 'libelle',
            // 'by_reference' => false,
            'multiple' => true,
            'query_builder' => function (InventaireActifRepository $repo) {
                return $repo->lesActifsReformesSortedASC();
            },
        ]);
}

edit route in TypeActifController.php:

public function new(Request $request, TypeActifRepository $typeActifRepository, EntityManagerInterface $entityManager): Response
    {
        $typeActif = new TypeActif();
        $form = $this->createForm(TypeActifType::class, $typeActif);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $typeActifRepository->add($typeActif, true);

            return $this->redirectToRoute('app_type_actif_index', [], Response::HTTP_SEE_OTHER);
        }

        return $this->renderForm('type_actif/new.html.twig', [
            'type_actif' => $typeActif,
            'form' => $form,
        ]);
    }

Issue with 'by_reference':

Attempts to Resolve:

Expected Behavior:

The changes should be saved to the database when InventaireActif entities are added via the TypeActif form, without needing to set 'by_reference' => false.

Current Behavior:

Without 'by_reference' => false, the changes are not persisted. No error is displayed, and the logs show nothing out of the ordinary.


Solution

  • If by_reference is set to true (or is ommitted), Symfony's form system simply adds the selected entities to the specified property using the methods of the ArrayCollection class. If by_reference is set to false, it uses the custom add/remove methods you specified instead.

    And when saving the entity to the database, only changes made to the owning side of the ManyToMany relationship are saved. The owning side in your code is the $lesTypesActifs property in the InventaireActif class.

    In your form, without by_reference = false, you're only updating the $lesActifs property of a TypeActif entity, which is the inverse side of the relationship, so that's why the changes are not being saved. But if you add by_reference = false, the custom add/remove methods make sure the owning side is also updated, so then the changes are saved.

    I'm not sure if you also have a form that updates the InventaireActif entity by modifying its $lesTypeActif property, but if you do not, you could probably just switch the owning and inverse sides of your entities to get rid of by_reference = false:

    class TypeActif {
        /**
         * @ORM\ManyToMany(targetEntity=InventaireActif::class, inversedBy="lesTypesActifs")
         */
        private $lesActifs;
    
        ...
    }
    

    class InventaireActif {
        /**
         * @ORM\ManyToMany(targetEntity=TypeActif::class, mappedBy="lesActifs")
         */
        private $lesTypesActifs;
    
        ...
    }