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.
8 avril 2021 à 14h46
Bonjour,
Merci pour ce tuto, mais je rencontre un problème, le addXXX n’est jamais appelé.
Pourtant j’ai bien un CollectionType avec by_reference => false et j’ai mis un die dans la méthode et non, ça ne passe pas. Bien entendu j’ai le addXXX et le removeXXX par contre j’ai une relation manytomany et non onetomany.
Autre différence je suis en SF5 … est ce peut-être ça le problème ?
Auriez-vous une idée ?
Merci pour votre tuto
8 avril 2021 à 17h52
petit complément :
Pour que cela fonctionne le addXXX,je dois le faire dans le controller avant d’appeler le createForm, alors que je supposais qu’il se faisait automatiquement … ce n’est pas vraiment clair pour moi
27 avril 2021 à 15h38
Salut Pascall,
Je ne pense pas que Symfony 5 soit en cause. La documentation officielle indique toujours la même chose par rapport à cet attribut : https://symfony.com/doc/current/reference/forms/types/collection.html#by-reference
Y a t’il bien l’option allow_add = True ?
N’hésites pas à fournir un lien vers ton code source, ce sera plus simple pour débugger 🙂
19 octobre 2020 à 23h52
Pour quoi cette ligne $this->diplomes[] = $diplome; est avant cette ligne
$diplome->setUser($this);
Pour quoi pas le contraire, je pense qu’il faut attacher l’utilisateur au diplôme avant de mettre à jour les diplômes
20 octobre 2020 à 10h35
L’ordre n’a pas d’importance, on peut faire l’inverse aussi