-
-
Save Ocramius/3121916 to your computer and use it in GitHub Desktop.
<?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); | |
} | |
} |
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 thanks, updated!
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.
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!
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?
How do work with extra columns in associating table?
https://pt.stackoverflow.com/questions/207760/doctrine-relacionamento-manytomany-com-campos-extras
@szagot With extra fields it will not be a ManyToMany relation, it's necessary a new table.
@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.
TIL: This will knock your servers out with large tables.
$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 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).
Thanks a lot +1
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?
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.
A couple questions, maybe this is ignorance but...
- Why do a negative check in the IF?
- Why return nothing as opposed to
true
orfalse
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?
@kemo I think I found a way to improve performance for large tables. It looks like by default $collection->contains
loads all the related objects in memory. I set the fetch
option (in the ManyToMany binding) to EXTRA_LAZY
and that seemed to reduce the loaded entities quite drastically! See: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/tutorials/extra-lazy-associations.html
@haydenk a specific return value is outside the scope for this gist, which is simply demonstrating how to handle the bi-directional object associations.
Ultimately the return values would depend on your desired behavior. You could also return $this;
to allow for method chaining,
or return $this->someOtherMthod($object);
to perform additional business logic and it's return value. Really is up to you.
@haydenk I also usually do returns as soon as I can so the processor doesn't have to read the entire function before continuing.
Also, not doing that you often see unneeded extra indentation in your whole function.
How would you query for a user that is in multiple groups?
eg.
User1 is in Group1 and Group2,
User2 is in Group1,
User3 is in Group2
The query should return only User1.
@werdck I would look into Doctrine specific methods. In that scenario the ORM would load the data for you and groups would be a relationship you can search through the array or collection.
https://www.doctrine-project.org/projects/doctrine-collections/en/stable/index.html
@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