Search code examples
doctrine-ormmany-to-manyphp-5.6symfony3

How to edit/update a many to many relationship in Symfony 3 from the inverse side


I'm working on a bash script that create a simple project "from zero to CRUD" with some tool and Symfony 3 bin/console doctrine:generate:* It works fine but in the M:N association case i can't update data from the inverse side. I read some answers here and i started some tests but i'm confused among "cascade={"all"}" option, 'by_reference' => false and other suggests. What is the simplest way to do that starting from this basic example taken from the offical doctrine docs?

    /** @Entity */
class User {
    // ...

    /**
     * Many Users have Many Groups.
     * @ManyToMany(targetEntity="Group", inversedBy="users")
     */
    private $groups;

    public function __construct() {
        $this->groups = new \Doctrine\Common\Collections\ArrayCollection();
    }

    // ...
}

/** @Entity */
class Group {

    // ...
    /**
     * Many Groups have Many Users.
     * @ManyToMany(targetEntity="User", mappedBy="groups")
     */
    private $users;

    public function __construct() {
        $this->users = new \Doctrine\Common\Collections\ArrayCollection();
    }

    // ...
}

Solution

  • Here's a complete example of a many-to-many relationship in Symfony and Doctrine. I usually use yaml instead of annotations. So you'll have to convert yourself the code using annotations if you want using annotations.

    # AppBundle/Entity/User.php
    //the id + more fields here if needed
    //getters and setters for the other fields
    
    /**
     * @var \Doctrine\Common\Collections\Collection
     */
    private $groups;
    
    public function __construct()
    {
        $this->groups = new \Doctrine\Common\Collections\ArrayCollection();
    }
    
    /**
     * Add group
     *
     * @param \AppBundle\Entity\Group $group
     *
     * @return User
     */
    public function addGroup(\AppBundle\Entity\Group $group)
    {
        if ($this->groups->contains($group)) {
            return;
        }
    
        //those two lines of code are the one you are seeking for, for saving both the owning side and the inverse side
        $this->groups[] = $group;
        $group->addUser($this);
    
        return $this;
    }
    
    /**
     * Remove group
     *
     * @param \AppBundle\Entity\Group $group
     */
    public function removeGroup(\AppBundle\Entity\Group $group)
    {
        if (!$this->groups->contains($group)) {
            return;
        }
    
        //those are the lines for removing the owning side and the inverse side
        $this->groups->removeElement($group);
        $group->removeUser($this);
    }
    
    /**
     * Get groups
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getGroups()
    {
        return $this->groups;
    }
    
    # AppBundle/Resources/config/doctrine/User.orm.yml
    AppBundle\Entity\User:
    type: entity
    table: users
    repositoryClass: AppBundle\Repository\UserRepository
    id:
        id:
            type: integer
            id: true
            generator:
                strategy: AUTO
    manyToMany:
        groups:
            targetEntity: AppBundle\Entity\Group
            inversedBy: users
            joinTable:
                name: groups_users
                joinColumns:
                    user_id:
                        referencedColumnName: id
                        nullable: true
                inverseJoinColumns:
                    group_id:
                        referencedColumnName: id
            cascade: ['persist', 'remove']
            fetch: EAGER
    fields:
        # more fields here if needed
    lifecycleCallbacks: {  }
    
    # AppBundle/Entity/Group.php
    //the id + more fields here if needed
    //getters and setters for the other fields
    
    /**
     * @var \Doctrine\Common\Collections\Collection
     */
    private $users;
    
    public function __construct()
    {
        $this->users = new \Doctrine\Common\Collections\ArrayCollection();
    }
    
    /**
     * Add user
     *
     * @param \AppBundle\Entity\User $user
     *
     * @return Group
     */
    public function addUser(\AppBundle\Entity\User $user)
    {
        if ($this->users->contains($user)) {
            return;
        }
    
        //these lines saves both the inverse side and the owning side
        $this->users[] = $user;
        $user->addGroup($this);
    
        return $this;
    }
    
    /**
     * Remove user
     *
     * @param \AppBundle\Entity\User $user
     */
    public function removeUser(\AppBundle\Entity\User $user)
    {
        if (!$this->users->contains($user)) {
            return;
        }
    
        //these lines remove both the inverse side and the owning side
        $this->users->removeElement($user);
        $user->removeGroup($this);
    }
    
    /**
     * Get users
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getUsers()
    {
        return $this->users;
    }
    
    # AppBundle/Resources/config/doctrine/Group.orm.yml
    AppBundle\Entity\Group:
    type: entity
    table: groups
    repositoryClass: AppBundle\Repository\GroupRepository
    id:
        id:
            type: integer
            id: true
            generator:
                strategy: AUTO
    manyToMany:
        users:
            targetEntity: AppBundle\Entity\User
            mappedBy: groups
            cascade: ['persist']
            fetch: EAGER
    fields:
        # more fields here if needed
    lifecycleCallbacks: {  }
    

    I hope I didn't made any mistake, as I adapted my local Category - Product example on yours.

    Next, you need to have, in each form type, a field to be able to select groups for a user, and users for a group.

    # AppBundle/Form/UserType.php
    $builder
        ->add('name') // this is a field I used in my local example, you can add yours
        ->add('groups', EntityType::class, [
            'class' => 'AppBundle:Group',
            'placeholder' => 'Choose a Group',
            'query_builder' => function (EntityRepository $er) {
                return $er->createQueryBuilder('g')
                    ->orderBy('g.name', 'DESC');
            },
            'choice_label' => 'name',
            'multiple'=>true,
            'expanded'=>false,
            'by_reference' => false,
        ])
    
    # AppBundle/Form/GroupType.php
    $builder
        ->add('name')
        ->add('users', EntityType::class, [
            'class' => 'AppBundle:User',
            'placeholder' => 'Choose User',
            'query_builder' => function (EntityRepository $er) {
                return $er->createQueryBuilder('u')
                    ->orderBy('u.name', 'DESC');
            },
            'choice_label' => 'name',
            'multiple'=>true,
            'expanded'=>false,
            'by_reference' => false,
        ])
    

    Before updating the schema, you need to create both your entities, without any mapping. Then update the schema, then proceed with creating the many-to-many relation between the entities, and update the schema once again, to apply the relation. Give it a try, and let us know if it worked.