Symfony : gestion des collections dans les formulaires

Symfony : gestion des collections dans les formulaires
09 Juin 2016


Prenons un exemple simple :

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

Notre entité User :

Entity/User.php :

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 :

Entity/Diplome.php :

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 :

Form/UserType.php :

$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 diplome, j'ai la fameuse erreur :

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

 

Apparement, la fonction addDiplome de mon entité User n'est pas appelée ...

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

Form/UserType.php :

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

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 ... (Et j'ai vu pire aussi ! Parfois c'est dans le controler 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.

Form/UserType.php :

$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);

Autre explication sur la documentation de symfony.

 

Commentaires

Rakoto
22 Juin 2018 20:18

ok (y)

Anouchka
23 Mai 2018 13:18

Bonjour
Merci pour votre tuto !
J'essaie de vous suivre mais je n'aboutit pas au résultat. Certaines parties sont floues pour moi. Pourrai-on avoir le code github ?
Merci

Sgothan
11 Avril 2018 12:18

Merci beaucoup,j'ai maintenant un formulaire opérationnel avec une collection !

Rémi POIGNON admin
10 Avril 2018 12:21

Non pas besoin de créer le prototype toi même, il est généré par Symfony, si tu souhaite le modifier, il faut t'en occuper dans les thèmes de formulaire mais par défaut, un prototype est proposé.
Si tu consulte le thème de formulaire de base (dans vendor/symfony/symfony/src/Symfony/Bridge/Twig/Resources/views/form/form_div_layout.html.twig), tu peux voir que le bloc "collection_widget" inclut le prototype dans un attribut "data-prototype".

Pour la partie JS, je te conseil d'utiliser un script déjà tout fait (exemple : https://symfony-collection.fuz.org/symfony3/). A toi de voir selon ton besoin pour rechercher ou construire le script JS qui va bien. Tu peux aussi trouver un exemple de JS pour gérer les collections sur la documentation officiel qui te propose une bonne base pour construire ton propre script JS (https://symfony.com/doc/current/reference/forms/types/collection.html)

Sgothan
10 Avril 2018 11:47

Très bonne explication, c'est juste dommage que tu n'expliques pas comment on peux ajouter et supprimer les collections à l'aide du JS.
Car je me pose la question si il faut créer le prototype sois-même malgrès le fait que l'option prototype soit mise à true.
Merci !

Loïc
02 Mars 2018 16:25

Merci beaucoup

Samuel
25 Février 2018 23:03

Je viens de passer une partie de ma journée à chercher pourquoi ça ne fonctionnait pas. Tu m'as apporté la solution. Merci beaucoup.

Rémi POIGNON admin
13 Juin 2017 19:47

Oups, merci, c'est modifié

Baptiste
23 Mai 2017 14:42

Hello,

Dans ton entité User, "removeImage" doit être une coquille ;-)

Rémi POIGNON admin
15 Avril 2017 19:19

Bonjour, oui c'est toujours d'actualité sur Symfony 3, n'hésitez pas à me contacter en MP pour trouver la cause du problème :-)

niryhna
05 Avril 2017 17:21

Bonjour,
Merci pour cette article, mais j'ai une question, c'est valable version symfony donc, est valable sur symfony3?
J'ai essayé de suivre avec mon et je n'arrive pas toujours au resultat