Symfony : gestion des collections dans les formulaires

gestion des collections dans les formulaires

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 :

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

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());
        }
        
    }
);

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 … (Et j’ai vu pire aussi ! 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.

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.

Commentaire (19)

  • Symdebut| 13 mars 2019

    J’ai une entite collection que j’essaye de modifier avec la boite modale en javascript mais il ne modifie pas mais cree plutot une nouvelle ligne. Comment faire?

  • niryhna| 5 avril 2017

    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

    • Rémi| 15 avril 2017

      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 🙂

  • Baptiste| 23 mai 2017

    Hello,

    Dans ton entité User, « removeImage » doit être une coquille 😉

  • Samuel| 28 février 2018

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

  • Loïc| 2 mars 2018

    Merci beaucoup

  • Sgothan| 10 avril 2018

    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 !

    • Rémi| 10 avril 2018

      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| 11 avril 2018

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

  • Anouchka| 23 mai 2018

    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

  • parfait| 3 janvier 2019

    merci beaucoup !

  • Sany| 2 mars 2019

    Bonjour, merci pour ce tuto, moi à mon niveau je voudrai savoir s’il est possible d’avoir deux attributs dans l’entité Diplome et les manipuler de façon distincte sur le rendu
    par exemple afficher

    {{ form_widget(form.diplomes.nom }}
    {{ form_widget(form.diplomes.annees}}

    Merci !!!

    • Rémi| 5 avril 2019

      Bonjour Sany,
      C’est tout à fait possible, je ne sais pas si c’est la meilleur façon de faire mais moi je fais comme ça :
      Dans mon form.html.twig :
       

        {% form_theme form ‘user/formTheme.html.twig’ %}

        {{ form_row(form.name) }}
        {{ form_row(form.firstname) }}
        {{ form_row(form.diplomes) }}
        {{ form_rest(form) }}

       

      Et je créé un fichier de personnalisation de formulaire : user/formTheme.html.twig :

       

      {% block form_widget %}

        {% if form.vars.unique_block_prefix == ‘_user_diplome_entry’ %}
          {{ block(‘collection_diplome’) }}
        {% else %}
          {{ parent() }}
        {% endif %}

      {% endblock form_widget %}

       

      {% block collection_widget %}

        {% if prototype is defined and form.vars.unique_block_prefix == ‘_user_diplome’ %}
          {% set attr = attr|merge({‘data-prototype’: block(‘collection_diplome’) }) %}
          {{ block(‘form_widget’) }}
        {% else %}
          {{ parent() }}
        {% endif %}

      {% endblock collection_widget %}

       

      {# C’est ici que je personnalise l’affichage de la collection de diplome #}
      {% block collection_diplome %}

        {% if prototype is defined %}
          {{ form_row(prototype.nom) }}
          {{ form_row(prototype.annees) }}
        {% else%}
          {{ form_row(form.children.nom) }}
          {{ form_row(form.children.annees) }}
        {% endif %}

      {% endblock collection_diplome %}

  • Famas| 19 octobre 2020

    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

  • pascall| 8 avril 2021

    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

    • pascall| 8 avril 2021

      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

    • Rémi| 27 avril 2021

      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 🙂

  • Laisser un commentaire

    Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *