Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Doctrine 2 ManyToMany - the correct way
<?php
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @ORM\Entity()
* @ORM\Table(name="user")
*/
class User
{
/**
* @var int|null
* @ORM\Id()
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer", name="id")
*/
protected $id;
/**
* @var \Doctrine\Common\Collections\Collection|UserGroup[]
*
* @ORM\ManyToMany(targetEntity="UserGroup", inversedBy="users")
* @ORM\JoinTable(
* name="user_usergroup",
* joinColumns={
* @ORM\JoinColumn(name="user_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* @ORM\JoinColumn(name="usergroup_id", referencedColumnName="id")
* }
* )
*/
protected $userGroups;
/**
* Default constructor, initializes collections
*/
public function __construct()
{
$this->userGroups = new ArrayCollection();
}
/**
* @param UserGroup $userGroup
*/
public function addUserGroup(UserGroup $userGroup)
{
if ($this->userGroups->contains($userGroup)) {
return;
}
$this->userGroups->add($userGroup);
$userGroup->addUser($this);
}
/**
* @param UserGroup $userGroup
*/
public function removeUserGroup(UserGroup $userGroup)
{
if (!$this->userGroups->contains($userGroup)) {
return;
}
$this->userGroups->removeElement($userGroup);
$userGroup->removeUser($this);
}
}
<?php
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @ORM\Entity()
* @ORM\Table(name="usergroup")
*/
class UserGroup
{
/**
* @var int|null
* @ORM\Id()
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer", name="id")
*/
protected $id;
/**
* @var \Doctrine\Common\Collections\Collection|User[]
*
* @ORM\ManyToMany(targetEntity="User", mappedBy="userGroups")
*/
protected $users;
/**
* Default constructor, initializes collections
*/
public function __construct()
{
$this->users = new ArrayCollection();
}
/**
* @param User $user
*/
public function addUser(User $user)
{
if ($this->users->contains($user)) {
return;
}
$this->users->add($user);
$user->addUserGroup($this);
}
/**
* @param User $user
*/
public function removeUser(User $user)
{
if (!$this->users->contains($user)) {
return;
}
$this->users->removeElement($user);
$user->removeUserGroup($this);
}
}
@basz

This comment has been minimized.

Show comment Hide comment
@basz

basz Dec 3, 2012

mappedBy="blocks"??? shouldn't that be mappedBy="groups"

basz commented Dec 3, 2012

mappedBy="blocks"??? shouldn't that be mappedBy="groups"

@basz

This comment has been minimized.

Show comment Hide comment
@basz

basz Dec 3, 2012

idem for inversedBy="blocks"?

basz commented Dec 3, 2012

idem for inversedBy="blocks"?

@Ocramius

This comment has been minimized.

Show comment Hide comment
@Ocramius

Ocramius Feb 19, 2013

@basz fixed

Owner

Ocramius commented Feb 19, 2013

@basz fixed

@JulienDotDev

This comment has been minimized.

Show comment Hide comment
@JulienDotDev

JulienDotDev Jun 24, 2013

In User.php :

"protected $groups ;"

but you are using the var $userGroups everywher :

"$this->userGroups = new ArrayCollection();"
...

In User.php :

"protected $groups ;"

but you are using the var $userGroups everywher :

"$this->userGroups = new ArrayCollection();"
...

@Ocramius

This comment has been minimized.

Show comment Hide comment
@Ocramius

Ocramius Aug 18, 2014

Owner

Ocramius commented Aug 18, 2014

@sliman1345

This comment has been minimized.

Show comment Hide comment
@sliman1345

sliman1345 Sep 11, 2014

@Ocramius
No, this not Fixed, this is infinite loop !!

 "addUser"  => "addUserGroup" => "addUser" => "addUserGroup" => "addUser" => ...

Just remove "$user->addUserGroup($this);" from the Entity "UserGroup", its fine.
And keep "$userGroup->addUser($this);" in "User" Entity (synchronously updating inverse side).

@Ocramius
No, this not Fixed, this is infinite loop !!

 "addUser"  => "addUserGroup" => "addUser" => "addUserGroup" => "addUser" => ...

Just remove "$user->addUserGroup($this);" from the Entity "UserGroup", its fine.
And keep "$userGroup->addUser($this);" in "User" Entity (synchronously updating inverse side).

@Ocramius

This comment has been minimized.

Show comment Hide comment
@Ocramius

Ocramius Jan 2, 2015

@sliman1345 I don't see an infinite loop here: the loop is terminated because of early returns in case no operation has to be applied

Owner

Ocramius commented Jan 2, 2015

@sliman1345 I don't see an infinite loop here: the loop is terminated because of early returns in case no operation has to be applied

@fyrye

This comment has been minimized.

Show comment Hide comment
@fyrye

fyrye Jan 28, 2015

UserGroups.php

@ORM\ManyToMany(targetEntity="User", mappedBy="groups")

There is no property named $groups, shouldn't it be

@ORM\ManyToMany(targetEntity="User", mappedBy="userGroups")

fyrye commented Jan 28, 2015

UserGroups.php

@ORM\ManyToMany(targetEntity="User", mappedBy="groups")

There is no property named $groups, shouldn't it be

@ORM\ManyToMany(targetEntity="User", mappedBy="userGroups")
@Ocramius

This comment has been minimized.

Show comment Hide comment
@Ocramius

Ocramius Mar 30, 2015

@fyrye thanks, updated!

Owner

Ocramius commented Mar 30, 2015

@fyrye thanks, updated!

@Nijusan

This comment has been minimized.

Show comment Hide comment
@Nijusan

Nijusan May 12, 2016

so i implemented products and categories with many to many the same way as described, but something i am missing...
when i want all categories of a product ($product-getCategories()) i only get a persistant-collection that is not initialized. so to really get an array of all categories i need to initialize it... ? isnt there a way that this happens automatically as it is with other relations?

on stackoverflow i read something about fetch=EAGER, but then the query for collecting the categories is always fired, even when i am not calling ->getCategories()

Nijusan commented May 12, 2016

so i implemented products and categories with many to many the same way as described, but something i am missing...
when i want all categories of a product ($product-getCategories()) i only get a persistant-collection that is not initialized. so to really get an array of all categories i need to initialize it... ? isnt there a way that this happens automatically as it is with other relations?

on stackoverflow i read something about fetch=EAGER, but then the query for collecting the categories is always fired, even when i am not calling ->getCategories()

@rogerwalt

This comment has been minimized.

Show comment Hide comment
@rogerwalt

rogerwalt May 23, 2016

You could always use the orm:generate-entities command, see here. If your associations are correct, this will produce the correct constructors and getter/setter methods for your entities. Also, the orm:validate-schema command will tell you what is wrong with your entity relations.

You could always use the orm:generate-entities command, see here. If your associations are correct, this will produce the correct constructors and getter/setter methods for your entities. Also, the orm:validate-schema command will tell you what is wrong with your entity relations.

@Ocramius

This comment has been minimized.

Show comment Hide comment
@Ocramius

Ocramius Jul 7, 2016

Please don't use orm:generate-entities: basically means that there is no business logic in your entities.

The example is here to demonstrate how the associations should always be balanced from both sides

Owner

Ocramius commented Jul 7, 2016

Please don't use orm:generate-entities: basically means that there is no business logic in your entities.

The example is here to demonstrate how the associations should always be balanced from both sides

@jrjohnson

This comment has been minimized.

Show comment Hide comment
@jrjohnson

jrjohnson Sep 29, 2016

@Ocramius is there a correct way to set the users from a Collection? we have somethign like:

/**
 * @param Collection $users
 */
public function setUsers(Collection $users = null)
{
    $this->users = new ArrayCollection();
    if (is_null($users)) {
        return;
    }

    foreach ($users as $user) {
        $this->addUser($user);
    }
}

But I think this may be the culprit for many MySQL deadlock errors we are seeing.

@Ocramius is there a correct way to set the users from a Collection? we have somethign like:

/**
 * @param Collection $users
 */
public function setUsers(Collection $users = null)
{
    $this->users = new ArrayCollection();
    if (is_null($users)) {
        return;
    }

    foreach ($users as $user) {
        $this->addUser($user);
    }
}

But I think this may be the culprit for many MySQL deadlock errors we are seeing.

@gintares

This comment has been minimized.

Show comment Hide comment
@gintares

gintares Nov 11, 2016

THe full example needs forms and controller actions: showing how to add user to the group, and to add groupt to the user. Despite that only one side is owing, sometimes i need to add relation oppositely, from mapping side.

THe full example needs forms and controller actions: showing how to add user to the group, and to add groupt to the user. Despite that only one side is owing, sometimes i need to add relation oppositely, from mapping side.

@mikolajprzybysz

This comment has been minimized.

Show comment Hide comment
@mikolajprzybysz

mikolajprzybysz Dec 13, 2016

@Ocramius Parameter name in @ORM\JoinTable in User.php should be set to the name of the entity, not the name of the table itself. Otherwise you break php bin/console doctrine:schema:update and it will throw The table with name user_usergroup already exists!

Is:

* @ORM\JoinTable(
*  name="user_usergroup",
*  joinColumns={
*      @ORM\JoinColumn(name="user_id", referencedColumnName="id")
*  },
*  inverseJoinColumns={
*      @ORM\JoinColumn(name="usergroup_id", referencedColumnName="id")
*  }
* )

Should be (assuming your join entity class name is UserUsergroup):

* @ORM\JoinTable(
*  name="UserUsergroup",
*  joinColumns={
*      @ORM\JoinColumn(name="user_id", referencedColumnName="id")
*  },
*  inverseJoinColumns={
*      @ORM\JoinColumn(name="usergroup_id", referencedColumnName="id")
*  }
* )

@Ocramius Parameter name in @ORM\JoinTable in User.php should be set to the name of the entity, not the name of the table itself. Otherwise you break php bin/console doctrine:schema:update and it will throw The table with name user_usergroup already exists!

Is:

* @ORM\JoinTable(
*  name="user_usergroup",
*  joinColumns={
*      @ORM\JoinColumn(name="user_id", referencedColumnName="id")
*  },
*  inverseJoinColumns={
*      @ORM\JoinColumn(name="usergroup_id", referencedColumnName="id")
*  }
* )

Should be (assuming your join entity class name is UserUsergroup):

* @ORM\JoinTable(
*  name="UserUsergroup",
*  joinColumns={
*      @ORM\JoinColumn(name="user_id", referencedColumnName="id")
*  },
*  inverseJoinColumns={
*      @ORM\JoinColumn(name="usergroup_id", referencedColumnName="id")
*  }
* )
@chili512

This comment has been minimized.

Show comment Hide comment
@chili512

chili512 Dec 28, 2016

Following this example I was able to successfully extract the information from a ManyToMany table. Thanks!

Following this example I was able to successfully extract the information from a ManyToMany table. Thanks!

@smilesrg

This comment has been minimized.

Show comment Hide comment
@smilesrg

smilesrg Jan 8, 2017

Tried this solution, but doesn't work for me

An exception occurred while executing 'INSERT INTO image_tags (name) VALUES (?)' with params [\"newtag\"]:\n\nSQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'newtag' for key 'UNIQ_9D867EB85E237E06'",

smilesrg commented Jan 8, 2017

Tried this solution, but doesn't work for me

An exception occurred while executing 'INSERT INTO image_tags (name) VALUES (?)' with params [\"newtag\"]:\n\nSQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'newtag' for key 'UNIQ_9D867EB85E237E06'",
@souzadevinicius

This comment has been minimized.

Show comment Hide comment
@souzadevinicius

souzadevinicius Feb 21, 2017

@smilesrg Did you find a solution?

@smilesrg Did you find a solution?

@szagot

This comment has been minimized.

Show comment Hide comment

szagot commented May 26, 2017

@rodrigoferra

This comment has been minimized.

Show comment Hide comment
@rodrigoferra

rodrigoferra Aug 11, 2017

@szagot With extra fields it will not be a ManyToMany relation, it's necessary a new table.

@szagot With extra fields it will not be a ManyToMany relation, it's necessary a new table.

@fyrye

This comment has been minimized.

Show comment Hide comment
@fyrye

fyrye Sep 24, 2017

@mikolajprzybysz in a true many-to-many association, the UserUsergroup entity does not exist. That would be a One-to-Many and Many-to-One association.

fyrye commented Sep 24, 2017

@mikolajprzybysz in a true many-to-many association, the UserUsergroup entity does not exist. That would be a One-to-Many and Many-to-One association.

@kemo

This comment has been minimized.

Show comment Hide comment
@kemo

kemo Oct 9, 2017

TIL: This will knock your servers out with large tables.

kemo commented Oct 9, 2017

TIL: This will knock your servers out with large tables.

@nimasdj

This comment has been minimized.

Show comment Hide comment
@nimasdj

nimasdj Oct 13, 2017

@Ocramius

          $user = $em->find('Entities\User', 1);
          $userGroups = $user->getUserGroups();
          foreach($userGroups as $userGroup) {
                       $admin->removeUserGroup($userGroup);
          }
          $em->remove($user);

The user itself is deleted, but its related userGroup in relationship join table not. Why?

nimasdj commented Oct 13, 2017

@Ocramius

          $user = $em->find('Entities\User', 1);
          $userGroups = $user->getUserGroups();
          foreach($userGroups as $userGroup) {
                       $admin->removeUserGroup($userGroup);
          }
          $em->remove($user);

The user itself is deleted, but its related userGroup in relationship join table not. Why?

@fyrye

This comment has been minimized.

Show comment Hide comment
@fyrye

fyrye Oct 17, 2017

@nimasdj What is $admin? For your logic to function correctly per your example, you would use.

$user = $em->find('Entities\User', 1);
$userGroups = $user->getUserGroups();
foreach ($userGroups as $userGroup) {
    $userGroup->removeUser($user); //remove associated user from user groups
}
$em->remove($user);

However the $userGroup->removeUser($user) iteration is not needed if you have foreign key onDelete="CASCADE" specified on the entity join column (and in the database). Optionally you could also use orphan removal in your ManyToMany declaration to ensure the association is not recreated.

fyrye commented Oct 17, 2017

@nimasdj What is $admin? For your logic to function correctly per your example, you would use.

$user = $em->find('Entities\User', 1);
$userGroups = $user->getUserGroups();
foreach ($userGroups as $userGroup) {
    $userGroup->removeUser($user); //remove associated user from user groups
}
$em->remove($user);

However the $userGroup->removeUser($user) iteration is not needed if you have foreign key onDelete="CASCADE" specified on the entity join column (and in the database). Optionally you could also use orphan removal in your ManyToMany declaration to ensure the association is not recreated.

@peyman-mohamadpour

This comment has been minimized.

Show comment Hide comment
@peyman-mohamadpour

peyman-mohamadpour Nov 7, 2017

It would be helpful to have the same thing for OneToMany & ManyToOne (Bidirectional).

It would be helpful to have the same thing for OneToMany & ManyToOne (Bidirectional).

@wsenjer

This comment has been minimized.

Show comment Hide comment
@wsenjer

wsenjer Dec 25, 2017

Thanks a lot +1

wsenjer commented Dec 25, 2017

Thanks a lot +1

@akomm

This comment has been minimized.

Show comment Hide comment
@akomm

akomm Jan 12, 2018

Its model-wise safe. But am I correct, that this causes initialization of the collections on both sides?
Example:
group-add-user: selects selects all users from group (contains($user)) and then inverse-side, selects all groups from user (contains($group)).

Does doctrine - on "contains" - just make a query to "check" or does it initialize a collection? If the second is the case, it does only contain proxies until you actually access an item, right?

akomm commented Jan 12, 2018

Its model-wise safe. But am I correct, that this causes initialization of the collections on both sides?
Example:
group-add-user: selects selects all users from group (contains($user)) and then inverse-side, selects all groups from user (contains($group)).

Does doctrine - on "contains" - just make a query to "check" or does it initialize a collection? If the second is the case, it does only contain proxies until you actually access an item, right?

@vieweg

This comment has been minimized.

Show comment Hide comment
@vieweg

vieweg Mar 22, 2018

Change
use Doctrine\Common\Collections\ArrayCollection; for
use Doctrine\Common\Collections\Collection;

and the methods:
public function getUser() or getUserGroups(), for example, return a array() and not a PersistentCollection.

vieweg commented Mar 22, 2018

Change
use Doctrine\Common\Collections\ArrayCollection; for
use Doctrine\Common\Collections\Collection;

and the methods:
public function getUser() or getUserGroups(), for example, return a array() and not a PersistentCollection.

@haydenk

This comment has been minimized.

Show comment Hide comment
@haydenk

haydenk Apr 3, 2018

A couple questions, maybe this is ignorance but...

  1. Why do a negative check in the IF?
  2. Why return nothing as opposed to true or false like the Collection methods do?

I see this a lot

    /**
     * @param User $user
     */
    public function addUser(User $user)
    {
        if (!$this->users->contains($user)) {
            $this->users[] = $user;
        }
    }

    /**
     * @param User $user
     */
    public function removeUser(User $user)
    {
        if ($this->users->contains($user)) {
            $this->users->removeElement($user);
        }
    }

And I am just curious, why not this instead?

    /**
     * @param User $user
     */
    public function addUser(User $user)
    {
        if ($this->users->contains($user)) {
            return true;
        }

        return $this->users->add($user);
    }

    /**
     * @param User $user
     */
    public function removeUser(User $user)
    {
        if ($this->users->contains($user)) {
            return $this->users->removeElement($user);
        }
        
        return true;
    }

This way, no matter what, the add and remove methods always return a boolean.

I am genuinely curious if there is some compatibility reasons behind it or if some of that functionality just didn't exist at one time and everyone is used to it or whatever it may be?

haydenk commented Apr 3, 2018

A couple questions, maybe this is ignorance but...

  1. Why do a negative check in the IF?
  2. Why return nothing as opposed to true or false like the Collection methods do?

I see this a lot

    /**
     * @param User $user
     */
    public function addUser(User $user)
    {
        if (!$this->users->contains($user)) {
            $this->users[] = $user;
        }
    }

    /**
     * @param User $user
     */
    public function removeUser(User $user)
    {
        if ($this->users->contains($user)) {
            $this->users->removeElement($user);
        }
    }

And I am just curious, why not this instead?

    /**
     * @param User $user
     */
    public function addUser(User $user)
    {
        if ($this->users->contains($user)) {
            return true;
        }

        return $this->users->add($user);
    }

    /**
     * @param User $user
     */
    public function removeUser(User $user)
    {
        if ($this->users->contains($user)) {
            return $this->users->removeElement($user);
        }
        
        return true;
    }

This way, no matter what, the add and remove methods always return a boolean.

I am genuinely curious if there is some compatibility reasons behind it or if some of that functionality just didn't exist at one time and everyone is used to it or whatever it may be?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment