Prenons un exemple simple :

Nous avons des utilisateurs à qui il est possible d’ajouter des diplômes.

Notre entité User :

class User
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
    
    // ... Autres attributs : nom / prénom ...
    /**
     * @var Diplome
     *
     * @ORM\OneToMany(targetEntity="Diplome", mappedBy="user", cascade="all", orphanRemoval=true)
     * @Assert\Valid()
     * @OrderBy({"position" = "ASC"})
     */
    private $diplomes;
    
    
    /**
     * Constructor
     */
    public function __construct()
    {
        $this->diplomes = new ArrayCollection();
    }
    
    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }
    
    // ... Autres getter et setter
    
    /**
     * Add diplome
     *
     * @param Diplome $diplome
     *
     * @return User
     */
    public function addDiplome(Diplome $diplome)
    {
        $this->diplomes[] = $diplome;
        $diplome->setUser($this);
        return $this;
    }
    /**
     * Remove diplome
     *
     * @param Diplome $diplome
     */
    public function removeDiplome(Diplome $diplome)
    {
        $this->diplomes->removeElement($diplome);
    }
    /**
     * Get diplomes
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getDiplomes()
    {
        return $this->diplomes;
    }
}

On passe l’attribut orphanRemoval à true pour la relation OneToMany avec Diplome. OrphanRemoval va permettre de supprimer une entité Diplome lorsqu’elle est retirée de la collection de l’entité User. On indique aussi l’attribut cascade = all. Ceci permet qu’un évènement doctrine sur l’entité User déclanche en cascade le même évènement sur l’entité Diplome : on persite un utilisateur donc on persiste ses diplômes, on supprime un utilisateur donc on supprime ses diplômes.

Dans la fonction addDiplome, on ajoute la ligne :

$diplome->setUser($this);

On ajoute le diplôme à l’utilisateur, on doit alors informer le dipôme de l’utilisateur auquel il est lié.

Voici maintenant notre entité Diplome :

class Diplome
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255)
     *
     * @Assert\NotBlank()
     * @Assert\Length(max="255")
     */
    private $name;
    /**
     * @var User
     *
     * @ORM\ManyToOne(targetEntity="User", inversedBy="diplomes")
     * @ORM\JoinColumn(nullable=false)
     * @Assert\NotNull()
     */
    private $user;
    
    /**
     * Constructor
     *
     * @param User $user
     */
    public function __construct(User $user = null)
    {
        $this->user = $user;
    }
    
    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }
    /**
     * Set user
     *
     * @param User $user
     *
     * @return Diplome
     */
    public function setUser(User $user)
    {
        $this->user = $user;
        return $this;
    }
    /**
     * Get user
     *
     * @return User
     */
    public function getUser()
    {
        return $this->user;
    }
}

Notre formulaire qui va permettre d’ajouter un utilisateur avec ses diplômes :

$builder
    // ->add('nom', TextType::class)
    // Autres champs de l'entité User ...
    ->add('diplomes', CollectionType:class, array(
        'type' => DiplomeType::class,
        'allow_add' => true,
        'allow_delete' => true,
        'prototype' => true,
    ));

On ajoute un champ diplome de type Collection :

type : le type de champ de la collection, ici ce sera un autre formulaire de type diplome

allow_add : On veut pouvoir en ajouter

allow_delete : On veut pouvoir en supprimer

prototype : On veut qu’un prototype soit défini afin de pouvoir gérer la collection en javascript côté client.

Pour les autres options, voir la documentation sur les CollectionType.

Et là quand j’envoie mon formulaire après avoir ajouté un diplôme, j’ai la fameuse erreur :

Cette valeur ne doit pas être nulle.

Pour info, j’ai cette erreur parce que j’ai indiqué @Assert\NotNull() pour l’attribut user de l’entité Diplome, sans cette Assert, j’aurai eu une belle erreur Symfony : Intégrity constraint violation

Apparemment, la fonction addDiplome de mon entité User n’est pas appelée …

Pour contrer cette erreur, on retrouve des codes du style :

$builder->addEventListener(
    FormEvents::SUBMIT,
    function (FormEvent $event) {
        
        foreach ($event->getData()->getDiplomes() as $diplome) {
            $diplome->setUser($event->getData());
        }
        
    }
);

Attention, ce code n’est pas recommandé, utilises plutôt la solution décrite plus bas.

Explication : Au submit, on fait une boucle sur les diplômes de la collection et, pour chacun, on appelle la méthode setUser qui va les lier à notre object User.

C’est pas génial comme code … (Parfois c’est dans le contrôler qu’on peut trouver cette boucle sur les catégories).

Comment bien gérer cette collection ? L’attribut by_reference

Il existe un attribut by_reference qu’il est indiqué pour tout type de formulaire. Par défaut, cet attribut est passé à true.

En passant cet attribut à false, on force Symfony à appeler le setter de l’entité.

Dans le cas d’une collection, on force Symfony à appeler les méthodes add et remove de l’attribut.

Pour notre exemple, on rajoute by_reference = false.

$builder
    ->add('diplomes', CollectionType:class, array(
        'type' => DiplomeType::class,
        'allow_add' => true,
        'allow_delete' => true,
        'prototype' => true,
        'by_reference' => false
    ));

Maintenant, au submit, la méthode addDiplome de l’entité User sera appelée et ainsi la ligne suivante sera exécutée, permettant de lier le diplôme à l’utilisateur :

$diplome->setUser($this);

Source : Documentation de symfony.