Rémi

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.

Partagez cette page

Comment (14)

  • Avatar
    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?

  • Avatar
    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
      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 🙂

  • Avatar
    Baptiste| 23 mai 2017

    Hello,

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

  • Avatar
    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.

  • Avatar
    Loïc| 2 mars 2018

    Merci beaucoup

  • Avatar
    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
      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)

      • Avatar
        Sgothan| 11 avril 2018

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

  • Avatar
    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

  • Avatar
    parfait| 3 janvier 2019

    merci beaucoup !

  • Avatar
    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
      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 %}

  • Laisser un commentaire

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