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 commented Dec 3, 2012

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

basz commented Dec 3, 2012

idem for inversedBy="blocks"?

Owner

Ocramius commented Feb 19, 2013

@basz fixed

In User.php :

"protected $groups ;"

but you are using the var $userGroups everywher :

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

Owner

Ocramius commented Aug 18, 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).

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 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")
Owner

Ocramius commented Mar 30, 2015

@fyrye thanks, updated!

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

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.

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

@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.

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.

@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")
*  }
* )

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

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'",

@smilesrg Did you find a solution?

szagot commented May 26, 2017

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

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 commented Oct 9, 2017

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

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

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

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