Hi everyone,

Today i will show you how to make Many-to-Many relations in Doctrine. I will cover the default Many-to-Many relation and show you two techniques to setup different types of Many-to-Many relations.

Doctrine manual

First lets look at what the Doctrine manual has to offer. There are unidirectional relationships and bidirectional relationships. I usually use the bidirectional ones since there is no extra overhead in the database. You might want to choose the unidirectional ones in case you want to prevent a look up from one side to the other, or possibly for a slight performance increase on the PHP side (though i have not tested this!)

Creating relations

When taking the groups and user example from the manual make sure to add a relation on both object and then persist both of them before using flush, otherwise your association will not be stored properly.

    $user = new User();
    $user->setName('GI Joe');

    $group = new Group();
    $group->setName('The army');

    $group->addUser($user);
    $user->addGroup($group);
    // The objects are now related in PHP land

    $em->persist($user);
    $em->persist($group);
    $em->flush();
    // The objects are now related in the database and have been assigned ID's

Different kinds of relations

The group-user example does not tell you what kind of relationship there is between a group and a user. It's safe to assume that when this is not explicit we can say "a user belongs to a group" and "a group has many users". But there are many more scenario's where different kinds of relations are useful, for example "user is pending to be added to group" and "group has been suggested to user". I will show you two techniques in how to implement this, along with the pros and cons.

Technique 1: Multiple join-tables

The code below shows you how to setup multiple many-to-many join tables between the same entities. We can see the type of relationship by the variable names and the table names. Note that you still need to dress up this code by adding more annotations, getters/setters and the constructor.

class User {
    /**
     * @ManyToMany(targetEntity="Group", inversedBy="belongingToUsers")
     * @JoinTable(name="users_groups_belongs")
     **/
    protected $belongingToGroups;

    /**
     * @ManyToMany(targetEntity="Group", inversedBy="suggestedUsers")
     * @JoinTable(name="users_groups_suggested")
     **/
    protected $suggestedGroups;
}

class Group {
    /**
     * @ManyToMany(targetEntity="User", mappedBy="belongingToGroups")
     **/
    protected $belongingToUsers;

    /**
     * @ManyToMany(targetEntity="User", mappedBy="suggestedGroups")
     **/
    protected $suggestedUsers;
}

Pros: Easy to work with. Clear semantics. Fast.
Cons: Additional tables. Not suitable for many types of relations.

Technique 2: The association entity

The doctrine manual gives you a hint about this technique in the unidirectional relationships section.

Why are many-to-many associations less common? Because frequently you want to associate additional attributes with an association, in which case you introduce an association class. Consequently, the direct many-to-many association disappears and is replaced by one-to-many/many-to-one associations between the 3 participating classes.

This technique is not a true many-to-many anymore. But the One-to-Many / Many-to-One is a good way of emulating a Many-to-Many relationship. First let's look at some code how to set this up.

class User {
    /**
     * @OneToMany(targetEntity="UserGroup", mappedBy="user")
     **/
    private $userGroups;
}

class UserGroup {
    /**
     * @ManyToOne(targetEntity="User", inversedBy="userGroups")
     * @JoinColumn(name="user_id", referencedColumnName="id")
     **/
    protected $user;

    /**
     * @ManyToOne(targetEntity="Group", inversedBy="userGroups")
     * @JoinColumn(name="group_id", referencedColumnName="id")
     **/
    protected $group;

    /**
     * @Column(type="string", length=7, nullable=false)
     */
    protected $type;

    public function __construct($type = null) {
        $this->type = $type;
    }
}

class Group {
    /**
     * @OneToMany(targetEntity="UserGroup", mappedBy="group")
     **/
    protected $userGroups;
}

We can now define the type of relationship on the association entity. Either with a setType() method or passing it directly as argument to the constructor. I've chosen a string of length 7 to implement the "belong" and "suggest" type of relationships. If you prefer you can make this field of type INT and then use constants in your class like so:

class UserGroup {
    const BELONG = 0;
    const SUGGEST = 1;

    /**
     * @Column(type="integer", nullable=false)
     */
    protected $type = self::BELONGS; // Setting the default type
}

Pros: Many possibilities when describing a relationship.
Cons: More code.

Working with different kinds of relations

We will now look at how to work with the two different techniques and compare the code needed. This code assumes you have set up the proper getters and setters.

Setup code:

// We create new users and groups for the example
// Normally you would use already existing ones
$user = new User();
$group = New Group();

Suggesting a new group to a user. Technique 1

$user->addSuggestedGroups($group);
$group->addSuggestedUsers($user);
$em->persist($user);
$em->persist($group);
$em->flush();

Suggesting a new group to a user. Technique 2

$userGroup = new UserGroup('suggest');
$user->addUserGroup($userGroup);
$userGroup->setUser($user);
$userGroup->setGroup($group);
$group->addUserGroup($userGroup);
$em->persist($userGroup);
$em->persist($user);
$em->persist($group);
$em->flush();

You see the association entity adds a lot more code. In another article published soon i will cover what code actually effects your database and show you some nice tricks to reduce repetitive code.