web-dev-qa-db-fra.com

Collection d'entités Symfony2 - comment ajouter / supprimer une association avec des entités existantes?

1. Aperçu rapide

1.1 Objectif

Ce que j'essaie de réaliser, c'est un outil utilisateur de création/modification. Les champs modifiables sont:

  • nom d'utilisateur (type: texte)
  • plainPassword (type: mot de passe)
  • email (type: email)
  • groupes (type: collection)
  • avoRoles (type: collection)

Remarque: la dernière propriété n'est pas nommée $ roles parce que ma classe User étend la classe User de FOSUserBundle et le remplacement des rôles a posé plus de problèmes. Pour les éviter, j'ai simplement décidé de stocker ma collection de rôles sous $ avoRoles .

1.2 Interface utilisateur

Mon modèle se compose de 2 sections:

  1. Formulaire utilisateur
  2. Tableau affichant $ userRepository-> findAllRolesExceptOwnedByUser ($ user);

Remarque: findAllRolesExceptOwnedByUser () est une fonction de référentiel personnalisée, renvoie un sous-ensemble de tous les rôles (ceux qui ne sont pas encore attribués à $ user).

1.3 Fonctionnalité souhaitée

1.3.1 Ajouter un rôle:

QUAND l'utilisateur clique sur le bouton "+" (ajouter) dans le tableau des rôles 
 PUIS jquery supprime cette ligne de la table des rôles 
 ET  jquery ajoute un nouvel élément de liste au formulaire utilisateur (liste avoRoles) 
 

1.3.2 Supprimer des rôles:

QUAND l'utilisateur clique sur le bouton "x" (supprimer) dans le formulaire utilisateur (liste avoRoles) 
 PUIS jquery supprime cet élément de liste du formulaire utilisateur (liste avoRoles) 
 ET  jquery ajoute une nouvelle ligne à la table des rôles 
 

1.3.3 Enregistrer les modifications:

QUAND l'utilisateur clique sur le bouton "Zapisz" (enregistrer) 
 PUIS le formulaire utilisateur soumet tous les champs (nom d'utilisateur, mot de passe, e-mail, avoRoles, groupes) 
 ET  enregistre les avoRoles en tant qu'ArrayCollection d'entités Role (relation ManyToMany) 
 ET  enregistre les groupes en tant qu'entités ArrayCollection of Role (relation ManyToMany) 
 

Remarque: seuls les rôles et groupes existants peuvent être attribués à l'utilisateur. Si pour une raison quelconque ils ne sont pas trouvés, le formulaire ne doit pas être validé.


2. Code

Dans cette section, je présente/ou décris brièvement le code derrière cette action. Si la description ne suffit pas et que vous avez besoin de voir le code, dites-le moi et je le collerai. Je ne colle pas tout cela en premier lieu pour éviter de vous spammer avec du code inutile.

2.1 Classe d'utilisateurs

Ma classe d'utilisateurs étend la classe d'utilisateurs FOSUserBundle.

namespace Avocode\UserBundle\Entity;

use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
use Avocode\CommonBundle\Collections\ArrayCollection;
use Symfony\Component\Validator\ExecutionContext;

/**
 * @ORM\Entity(repositoryClass="Avocode\UserBundle\Repository\UserRepository")
 * @ORM\Table(name="avo_user")
 */
class User extends BaseUser
{
    const ROLE_DEFAULT = 'ROLE_USER';
    const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\generatedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\ManyToMany(targetEntity="Group")
     * @ORM\JoinTable(name="avo_user_avo_group",
     *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="group_id", referencedColumnName="id")}
     * )
     */
    protected $groups;

    /**
     * @ORM\ManyToMany(targetEntity="Role")
     * @ORM\JoinTable(name="avo_user_avo_role",
     *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")}
     * )
     */
    protected $avoRoles;

    /**
     * @ORM\Column(type="datetime", name="created_at")
     */
    protected $createdAt;

    /**
     * User class constructor
     */
    public function __construct()
    {
        parent::__construct();

        $this->groups = new ArrayCollection();        
        $this->avoRoles = new ArrayCollection();
        $this->createdAt = new \DateTime();
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set user roles
     * 
     * @return User
     */
    public function setAvoRoles($avoRoles)
    {
        $this->getAvoRoles()->clear();

        foreach($avoRoles as $role) {
            $this->addAvoRole($role);
        }

        return $this;
    }

    /**
     * Add avoRole
     *
     * @param Role $avoRole
     * @return User
     */
    public function addAvoRole(Role $avoRole)
    {
        if(!$this->getAvoRoles()->contains($avoRole)) {
          $this->getAvoRoles()->add($avoRole);
        }

        return $this;
    }

    /**
     * Get avoRoles
     *
     * @return ArrayCollection
     */
    public function getAvoRoles()
    {
        return $this->avoRoles;
    }

    /**
     * Set user groups
     * 
     * @return User
     */
    public function setGroups($groups)
    {
        $this->getGroups()->clear();

        foreach($groups as $group) {
            $this->addGroup($group);
        }

        return $this;
    }

    /**
     * Get groups granted to the user.
     *
     * @return Collection
     */
    public function getGroups()
    {
        return $this->groups ?: $this->groups = new ArrayCollection();
    }

    /**
     * Get user creation date
     *
     * @return DateTime
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }
}

2.2 Classe de rôle

La classe My Role étend la classe Symfony Security Component Core Role.

namespace Avocode\UserBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Avocode\CommonBundle\Collections\ArrayCollection;
use Symfony\Component\Security\Core\Role\Role as BaseRole;

/**
 * @ORM\Entity(repositoryClass="Avocode\UserBundle\Repository\RoleRepository")
 * @ORM\Table(name="avo_role")
 */
class Role extends BaseRole
{    
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\generatedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", unique="TRUE", length=255)
     */
    protected $name;

    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $module;

    /**
     * @ORM\Column(type="text")
     */
    protected $description;

    /**
     * Role class constructor
     */
    public function __construct()
    {
    }

    /**
     * Returns role name.
     * 
     * @return string
     */    
    public function __toString()
    {
        return (string) $this->getName();
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name
     *
     * @param string $name
     * @return Role
     */
    public function setName($name)
    {      
        $name = strtoupper($name);
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string 
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Set module
     *
     * @param string $module
     * @return Role
     */
    public function setModule($module)
    {
        $this->module = $module;

        return $this;
    }

    /**
     * Get module
     *
     * @return string 
     */
    public function getModule()
    {
        return $this->module;
    }

    /**
     * Set description
     *
     * @param text $description
     * @return Role
     */
    public function setDescription($description)
    {
        $this->description = $description;

        return $this;
    }

    /**
     * Get description
     *
     * @return text 
     */
    public function getDescription()
    {
        return $this->description;
    }
}

2.3 Cours en groupe

Puisque j'ai le même problème avec les groupes qu'avec les rôles, je les saute ici. Si je fais travailler les rôles, je sais que je peux faire la même chose avec les groupes.

2.4 Contrôleur

namespace Avocode\UserBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\SecurityContext;
use JMS\SecurityExtraBundle\Annotation\Secure;
use Avocode\UserBundle\Entity\User;
use Avocode\UserBundle\Form\Type\UserType;

class UserManagementController extends Controller
{
    /**
     * User create
     * @Secure(roles="ROLE_USER_ADMIN")
     */
    public function createAction(Request $request)
    {      
        $em = $this->getDoctrine()->getEntityManager();

        $user = new User();
        $form = $this->createForm(new UserType(array('password' => true)), $user);

        $roles = $em->getRepository('AvocodeUserBundle:User')
                    ->findAllRolesExceptOwned($user);
        $groups = $em->getRepository('AvocodeUserBundle:User')
                    ->findAllGroupsExceptOwned($user);

        if($request->getMethod() == 'POST' && $request->request->has('save')) {
            $form->bindRequest($request);

            if($form->isValid()) {
                /* Persist, flush and redirect */
                $em->persist($user);
                $em->flush();
                $this->setFlash('avocode_user_success', 'user.flash.user_created');
                $url = $this->container->get('router')->generate('avocode_user_show', array('id' => $user->getId()));

                return new RedirectResponse($url);
            }
        }

        return $this->render('AvocodeUserBundle:UserManagement:create.html.twig', array(
          'form' => $form->createView(),
          'user' => $user,
          'roles' => $roles,
          'groups' => $groups,
        ));
    }
}

2.5 Référentiels personnalisés

Il n'est pas nécessaire de publier ceci car ils fonctionnent très bien - ils renvoient un sous-ensemble de tous les rôles/groupes (ceux qui ne sont pas attribués à l'utilisateur).

2.6 UserType

Type d'utilisateur:

namespace Avocode\UserBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class UserType extends AbstractType
{    
    private $options; 

    public function __construct(array $options = null) 
    { 
        $this->options = $options; 
    }

    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder->add('username', 'text');

        // password field should be rendered only for CREATE action
        // the same form type will be used for EDIT action
        // thats why its optional

        if($this->options['password'])
        {
          $builder->add('plainpassword', 'repeated', array(
                        'type' => 'text',
                        'options' => array(
                          'attr' => array(
                            'autocomplete' => 'off'
                          ),
                        ),
                        'first_name' => 'input',
                        'second_name' => 'confirm', 
                        'invalid_message' => 'repeated.invalid.password',
                     ));
        }

        $builder->add('email', 'email', array(
                        'trim' => true,
                     ))

        // collection_list is a custom field type
        // extending collection field type
        //
        // the only change is diffrent form name
        // (and a custom collection_list_widget)
        // 
        // in short: it's a collection field with custom form_theme
        // 
                ->add('groups', 'collection_list', array(
                        'type' => new GroupNameType(),
                        'allow_add' => true,
                        'allow_delete' => true,
                        'by_reference' => true,
                        'error_bubbling' => false,
                        'prototype' => true,
                     ))
                ->add('avoRoles', 'collection_list', array(
                        'type' => new RoleNameType(),
                        'allow_add' => true,
                        'allow_delete' => true,
                        'by_reference' => true,
                        'error_bubbling' => false,
                        'prototype' => true,
                     ));
    }

    public function getName()
    {
        return 'avo_user';
    }

    public function getDefaultOptions(array $options){

        $options = array(
          'data_class' => 'Avocode\UserBundle\Entity\User',
        );

        // adding password validation if password field was rendered

        if($this->options['password'])
          $options['validation_groups'][] = 'password';

        return $options;
    }
}

2.7 RoleNameType

Ce formulaire est censé rendre:

  • iD de rôle caché
  • Nom du rôle (LIRE UNIQUEMENT)
  • module caché (LIRE SEULEMENT)
  • description cachée (LIRE SEULEMENT)
  • supprimer le bouton (x)

Le module et la description sont rendus sous forme de champs masqués, car lorsque l'administrateur supprime un rôle d'un utilisateur, ce rôle doit être ajouté par jQuery à la table des rôles - et cette table contient des colonnes de module et de description.

namespace Avocode\UserBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class RoleNameType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder            
            ->add('', 'button', array(
              'required' => false,
            ))  // custom field type rendering the "x" button

            ->add('id', 'hidden')

            ->add('name', 'label', array(
              'required' => false,
            )) // custom field type rendering <span> item instead of <input> item

            ->add('module', 'hidden', array('read_only' => true))
            ->add('description', 'hidden', array('read_only' => true))
        ;        
    }

    public function getName()
    {
        // no_label is a custom widget that renders field_row without the label

        return 'no_label';
    }

    public function getDefaultOptions(array $options){
        return array('data_class' => 'Avocode\UserBundle\Entity\Role');
    }
}

3. Problèmes actuels/connus

3.1 Cas 1: configuration telle que citée ci-dessus

La configuration ci-dessus renvoie une erreur:

Property "id" is not public in class "Avocode\UserBundle\Entity\Role". Maybe you should create the method "setId()"?

Mais le setter pour ID ne devrait pas être requis.

  1. D'abord parce que je ne veux pas créer un nouveau rôle. Je veux simplement créer une relation entre les entités de rôle et d'utilisateur existantes.
  2. Même si je voulais créer un nouveau rôle, son ID devrait être généré automatiquement:

    / **

    • @ORM\Id
    • @ORM\Column (type = "integer")
    • @ORM\generatedValue (stratégie = "AUTO") */protected $ id;

3.2 Cas 2: ajout d'un setter pour la propriété ID dans l'entité Role

Je pense que c'est faux, mais je l'ai fait juste pour être sûr. Après avoir ajouté ce code à l'entité Role:

public function setId($id)
{
    $this->id = $id;
    return $this;
}

Si je crée un nouvel utilisateur et ajoute un rôle, alors ENREGISTRER ... Ce qui se passe est:

  1. Un nouvel utilisateur est créé
  2. Le nouvel utilisateur a un rôle avec l'ID souhaité attribué (yay!)
  3. mais le nom de ce rôle est remplacé par une chaîne vide (bummer!)

Évidemment, ce n'est pas ce que je veux. Je ne veux pas modifier/écraser les rôles. Je veux juste ajouter une relation entre eux et l'utilisateur.

3.3 Cas 3: solution de contournement suggérée par Jeppe

Lorsque j'ai rencontré ce problème pour la première fois, je me suis retrouvé avec une solution de contournement, la même que celle suggérée par Jeppe. Aujourd'hui (pour d'autres raisons), j'ai dû refaire mon formulaire/vue et la solution de contournement a cessé de fonctionner.

Quels changements dans Case3 UserManagementController -> createAction:

  // in createAction
  // instead of $user = new User
  $user = $this->updateUser($request, new User());

  //and below updateUser function


    /**
     * Creates mew iser and sets its properties
     * based on request
     * 
     * @return User Returns configured user
     */
    protected function updateUser($request, $user)
    {
        if($request->getMethod() == 'POST')
        {
          $avo_user = $request->request->get('avo_user');

          /**
           * Setting and adding/removeing groups for user
           */
          $owned_groups = (array_key_exists('groups', $avo_user)) ? $avo_user['groups'] : array();
          foreach($owned_groups as $key => $group) {
            $owned_groups[$key] = $group['id'];
          }

          if(count($owned_groups) > 0)
          {
            $em = $this->getDoctrine()->getEntityManager();
            $groups = $em->getRepository('AvocodeUserBundle:Group')->findById($owned_groups);
            $user->setGroups($groups);
          }

          /**
           * Setting and adding/removeing roles for user
           */
          $owned_roles = (array_key_exists('avoRoles', $avo_user)) ? $avo_user['avoRoles'] : array();
          foreach($owned_roles as $key => $role) {
            $owned_roles[$key] = $role['id'];
          }

          if(count($owned_roles) > 0)
          {
            $em = $this->getDoctrine()->getEntityManager();
            $roles = $em->getRepository('AvocodeUserBundle:Role')->findById($owned_roles);
            $user->setAvoRoles($roles);
          }

          /**
           * Setting other properties
           */
          $user->setUsername($avo_user['username']);
          $user->setEmail($avo_user['email']);

          if($request->request->has('generate_password'))
            $user->setPlainPassword($user->generateRandomPassword());  
        }

        return $user;
    }

Malheureusement, cela ne change rien. Les résultats sont soit CASE1 (sans ID setter) soit CASE2 (avec ID setter).

3.4 Cas 4: comme suggéré par l'utilisateur

Ajout de cascade = {"persist", "remove"} au mappage.

/**
 * @ORM\ManyToMany(targetEntity="Group", cascade={"persist", "remove"})
 * @ORM\JoinTable(name="avo_user_avo_group",
 *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
 *      inverseJoinColumns={@ORM\JoinColumn(name="group_id", referencedColumnName="id")}
 * )
 */
protected $groups;

/**
 * @ORM\ManyToMany(targetEntity="Role", cascade={"persist", "remove"})
 * @ORM\JoinTable(name="avo_user_avo_role",
 *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
 *      inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")}
 * )
 */
protected $avoRoles;

Et changer by_reference en false dans FormType:

// ...

                ->add('avoRoles', 'collection_list', array(
                        'type' => new RoleNameType(),
                        'allow_add' => true,
                        'allow_delete' => true,
                        'by_reference' => false,
                        'error_bubbling' => false,
                        'prototype' => true,
                     ));

// ...

Et garder le code de contournement suggéré dans 3.3 a changé quelque chose:

  1. L'association entre l'utilisateur et le rôle n'a pas été créée
  2. .. mais le nom de l'entité de rôle a été remplacé par une chaîne vide (comme dans 3.2)

Alors .. ça a changé quelque chose mais dans la mauvaise direction.

4. Versions

4.1 Symfony2 v2.0.15

4.2 Doctrine2 v2.1.7

4.3 Version FOSUserBundle: 6fb81861d84d460f1d070ceb8ec180aac841f7fa

5. Résumé

J'ai essayé de nombreuses approches différentes (ci-dessus ne sont que les plus récentes) et après des heures passées à étudier le code, à chercher sur Google et à chercher la réponse, je n'ai tout simplement pas pu faire fonctionner cela.

Toute aide sera fortement appréciée. Si vous avez besoin de savoir quelque chose, je publierai la partie du code dont vous avez besoin.

67
ioleo

Une année s'est donc écoulée et cette question est devenue très populaire. Symfony a changé depuis, mes compétences et mes connaissances se sont également améliorées, tout comme mon approche actuelle de ce problème.

J'ai créé un ensemble d'extensions de formulaire pour symfony2 (voir FormExtensionsBundle projet sur github) et elles incluent un type de formulaire pour gérer One/Many ToMany relations.

Lors de l'écriture de ces derniers, l'ajout de code personnalisé à votre contrôleur pour gérer les collections était inacceptable - les extensions de formulaire étaient censées être faciles à utiliser, fonctionnelles et faciliter la vie des développeurs, pas plus difficiles. Aussi .. rappelez-vous .. SEC!

J'ai donc dû déplacer le code d'association ajouter/supprimer ailleurs - et le bon endroit pour le faire était naturellement un EventListener :)

Jetez un œil au fichier EventListener/CollectionUploadListener.php pour voir comment nous gérons cela maintenant.

PS. Copier le code ici n'est pas nécessaire, la chose la plus importante est que des choses comme ça devraient être gérées dans EventListener.

10
ioleo

Je suis arrivé à la même conclusion qu'il y a quelque chose qui ne va pas avec le composant Form et je ne vois pas de moyen facile de le corriger. Cependant, j'ai trouvé une solution de contournement légèrement moins lourde qui est complètement générique; il n'a aucune connaissance codée en dur des entités/attributs, il corrigera donc toute collection qu'il rencontrera:

Méthode de contournement générique plus simple

Cela ne vous oblige pas à apporter des modifications à votre entité.

use Doctrine\Common\Collections\Collection;
use Symfony\Component\Form\Form;

# In your controller. Or possibly defined within a service if used in many controllers

/**
 * Ensure that any removed items collections actually get removed
 *
 * @param \Symfony\Component\Form\Form $form
 */
protected function cleanupCollections(Form $form)
{
    $children = $form->getChildren();

    foreach ($children as $childForm) {
        $data = $childForm->getData();
        if ($data instanceof Collection) {

            // Get the child form objects and compare the data of each child against the object's current collection
            $proxies = $childForm->getChildren();
            foreach ($proxies as $proxy) {
                $entity = $proxy->getData();
                if (!$data->contains($entity)) {

                    // Entity has been removed from the collection
                    // DELETE THE ENTITY HERE

                    // e.g. doctrine:
                    // $em = $this->getDoctrine()->getEntityManager();
                    // $em->remove($entity);

                }
            }
        }
    }
}

Appelez la nouvelle méthode cleanupCollections() avant de persister

# in your controller action...

if($request->getMethod() == 'POST') {
    $form->bindRequest($request);
    if($form->isValid()) {

        // 'Clean' all collections within the form before persisting
        $this->cleanupCollections($form);

        $em->persist($user);
        $em->flush();

        // further actions. return response...
    }
}
13
RobMasters

1. La solution de contournement

La solution de contournement suggérée par Jeppe Marianger-Lam est à l'heure actuelle la seule qui fonctionne à ma connaissance.

1.1 Pourquoi at-il cessé de fonctionner dans mon cas?

J'ai changé mon RoleNameType (pour d'autres raisons) en:

  • ID (caché)
  • nom (type personnalisé - étiquette)
  • module et description (caché, lecture seule)

Le problème est que mon étiquette de type personnalisée a rendu la propriété NAME sous la forme

 
 <span> nom du rôle </span> 
 

Et comme il n'était pas "en lecture seule", le composant FORM s'attendait à obtenir NAME dans POST.

Au lieu de cela, seul l'ID a été POSTé et le composant FORM a donc supposé que NAME est NULL.

Cela a conduit à CASE 2 (3.2) -> créer une association, mais en écrasant ROLE NAME avec une chaîne vide.

2. Alors, de quoi s'agit-il exactement?

2.1 Contrôleur

Cette solution de contournement est très simple.

Dans votre contrôleur, avant de VALIDER le formulaire, vous devez récupérer les identifiants d'entité publiés et obtenir les entités correspondantes, puis les définir sur votre objet.

// example action
public function createAction(Request $request)
{      
    $em = $this->getDoctrine()->getEntityManager();

    // the workaround code is in updateUser function
    $user = $this->updateUser($request, new User());

    $form = $this->createForm(new UserType(), $user);

    if($request->getMethod() == 'POST') {
        $form->bindRequest($request);

        if($form->isValid()) {
            /* Persist, flush and redirect */
            $em->persist($user);
            $em->flush();
            $this->setFlash('avocode_user_success', 'user.flash.user_created');
            $url = $this->container->get('router')->generate('avocode_user_show', array('id' => $user->getId()));

            return new RedirectResponse($url);
        }
    }

    return $this->render('AvocodeUserBundle:UserManagement:create.html.twig', array(
      'form' => $form->createView(),
      'user' => $user,
    ));
}

Et en dessous du code de contournement dans la fonction updateUser:

protected function updateUser($request, $user)
{
    if($request->getMethod() == 'POST')
    {
      // getting POSTed values
      $avo_user = $request->request->get('avo_user');

      // if no roles are posted, then $owned_roles should be an empty array (to avoid errors)
      $owned_roles = (array_key_exists('avoRoles', $avo_user)) ? $avo_user['avoRoles'] : array();

      // foreach posted ROLE, get it's ID
      foreach($owned_roles as $key => $role) {
        $owned_roles[$key] = $role['id'];
      }

      // FIND all roles with matching ID's
      if(count($owned_roles) > 0)
      {
        $em = $this->getDoctrine()->getEntityManager();
        $roles = $em->getRepository('AvocodeUserBundle:Role')->findById($owned_roles);

        // and create association
        $user->setAvoRoles($roles);
      }

    return $user;
}

Pour que cela fonctionne, votre SETTER (dans ce cas dans l'entité User.php) doit être:

public function setAvoRoles($avoRoles)
{
    // first - clearing all associations
    // this way if entity was not found in POST
    // then association will be removed

    $this->getAvoRoles()->clear();

    // adding association only for POSTed entities
    foreach($avoRoles as $role) {
        $this->addAvoRole($role);
    }

    return $this;
}

3. Réflexions finales

Pourtant, je pense que cette solution de contournement fait le travail qui

$form->bindRequest($request);

devrait faire! Soit je fais quelque chose de mal, soit le type de formulaire Collection de symfony n'est pas complet.

Il y a des changements majeurs dans le composant Form à venir dans symfony 2.1, j'espère que ce sera corrigé.

PS. Si c'est moi qui fais quelque chose de mal ...

... veuillez poster la façon dont cela devrait être fait! Je serais heureux de voir une solution rapide, facile et "propre".

PS2. Remerciements particuliers à:

Jeppe Marianger-Lam et convivial (à partir de # symfony2 sur IRC). Vous avez été très utile. À votre santé!

8
ioleo

C'est ce que j'ai fait auparavant - je ne sais pas si c'est la "bonne" façon de le faire, mais cela fonctionne.

Lorsque vous obtenez les résultats du formulaire soumis (c'est-à-dire juste avant ou juste après if($form->isValid())), demandez simplement la liste des rôles, puis supprimez-les tous de l'entité (en enregistrant la liste en tant que variable). Avec cette liste, parcourez-les tout simplement, demandez au référentiel l'entité de rôle qui correspond aux ID et ajoutez-les à votre entité utilisateur avant de persist et flush.

Je viens de parcourir la documentation Symfony2 parce que je me souvenais de quelque chose à propos de prototype pour les collections de formulaires, et cela s'est révélé: http://symfony.com/doc/current/cookbook/form/form_collections.html - Il contient des exemples sur la façon de gérer correctement l'ajout et la suppression javascript des types de collection dans les formulaires. Essayez peut-être d'abord cette approche, puis essayez ce que j'ai mentionné ci-dessus si vous ne pouvez pas le faire fonctionner :)

6
Jeppe Mariager-Lam

Vous avez besoin de plus d'entités:
[~ # ~] utilisateur [~ # ~]
id_user (type: entier)
nom d'utilisateur (type: texte)
plainPassword (type: mot de passe)
email (type: email)


GROUPES
id_group (type: entier)
description (type: texte)


AVOROLES
id_avorole (type: entier)
description (type: texte)


* SER_GROUP *
id_user_group (type: entier)
id_user (type: entier) (il s'agit de l'id sur l'entité utilisateur)
id_group (type: entier) (c'est l'id sur l'entité de groupe)


* SER_AVOROLES *
id_user_avorole (type: entier)
id_user (type: entier) (il s'agit de l'id sur l'entité utilisateur)
id_avorole (type: entier) (c'est l'identifiant sur l'entité avorole)


Vous pouvez par exemple avoir quelque chose comme ceci:
utilisateur:
id: 3
nom d'utilisateur: john
plainPassword: johnpw
e-mail: [email protected]


groupe:
id_group: 5
description: groupe 5


groupe_utilisateurs:
id_user_group: 1
id_user: 3
id_group: 5
* cet utilisateur peut avoir plusieurs groupes donc sur une autre ligne *

0
user1895187