By default, self-referencing ManyToMany
relationships under Doctrine involve an owning side and an inverse side, as explained in the documentation.
Is there a way to implement a reciprocal association whithout difference between both sides?
Following the example in the docs:
<?php /** @Entity **/ class User { // ... /** * @ManyToMany(targetEntity="User") **/ private $friends; public function __construct() { $this->friends = new \Doctrine\Common\Collections\ArrayCollection(); } // ... }
So, adding entity1
to entity2
s friends
implies that entity2
will be in entity1
s friends.
There are a number of ways to solve this problem, all depending on what the requirements for the "friends" relation are.
Unidirectional
A simple approach would be to use a unidirectional ManyToMany association, and treat it as if it where a bidirectional one (keeping both sides in sync):
/** * @Entity */ class User { /** * @Id * @Column(type="integer") */ private $id; /** * @ManyToMany(targetEntity="User") * @JoinTable(name="friends", * joinColumns={@JoinColumn(name="user_a_id", referencedColumnName="id")}, * inverseJoinColumns={@JoinColumn(name="user_b_id", referencedColumnName="id")} * ) * @var \Doctrine\Common\Collections\ArrayCollection */ private $friends; /** * Constructor. */ public function __construct() { $this->friends = new \Doctrine\Common\Collections\ArrayCollection(); } /** * @return array */ public function getFriends() { return $this->friends->toArray(); } /** * @param User $user * @return void */ public function addFriend(User $user) { if (!$this->friends->contains($user)) { $this->friends->add($user); $user->addFriend($this); } } /** * @param User $user * @return void */ public function removeFriend(User $user) { if ($this->friends->contains($user)) { $this->friends->removeElement($user); $user->removeFriend($this); } } // ... }
When you call $userA->addFriend($userB)
, $userB
will be added to the friends-collection in $userA
, and $userA
will be added to the friends-collection in $userB
.
It will also result in 2 records added to the "friends" table (1,2 and 2,1). While this can be seen as duplicate data, it will simplify your code a lot. For example when you need to find all friends of $userA
, you can simply do:
SELECT u FROM User u JOIN u.friends f WHERE f.id = :userId
No need to check 2 different properties as you would with a bidirectional association.
Bidirectional
When using a bidirectional association the User
entity will have 2 properties, $myFriends
and $friendsWithMe
for example. You can keep them in sync the same way as described above.
The main difference is that on a database level you'll only have one record representing the relationship (either 1,2 or 2,1). This makes "find all friends" queries a bit more complex because you'll have to check both properties.
You could of course still use 2 records in the database by making sure addFriend()
will update both $myFriends
and $friendsWithMe
(and keep the other side in sync). This will add some complexity in your entities, but queries become a little less complex.
OneToMany / ManyToOne
If you need a system where a user can add a friend, but that friend has to confirm that they are indeed friends, you'll need to store that confirmation in the join-table. You then no longer have a ManyToMany association, but something like User
<- OneToMany -> Friendship
<- ManyToOne -> User
.
You can read my blog-posts on this subject:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With