cult3

Working with relationships in Doctrine 2

Jul 21, 2014

Table of contents:

  1. The different types of relationships
  2. Unidirectional or Bidirectional relationships
  3. Understanding the Owning Side and the Inverse Side
  4. Using Collections
  5. Defining relationships
  6. Ordering Collections
  7. Conclusion

Last week we looked at setting up the User entity in Doctrine 2. Entities in Doctrine 2 are different to those in Eloquent in a number of ways. Firstly, they are just plain PHP objects and so they don’t have any responsibility when it comes to how they are stored.

Secondly they have annotations that describe each property of the class. These annotations are then used to map the entity to the database. In last week’s tutorial we looked at how we can create the database schema simply by reading the entity class.

In this week’s tutorial I want to continue exploring Doctrine 2 and show you how relationships between entities work.

The different types of relationships

Before I jump into how Doctrine 2 relationships work, first a quick refresher on the types of relationships we have available to us. In order to correctly design a database, you need to have a good understanding of the different types of relationships you have at hand.

One-to-One

A One-to-One relationship is where one entity is related to exactly one other entity. For example, a user would be related to one token.

One-to-Many

A One-to-Many relationship is where one entity is related to many other entities. For example, a user would have many posts.

Many-to-Many

And finally a Many-to-Many relationship is where many entities are related to many other entities. So for example, a user would belong to many groups and those groups would have many users.

Unidirectional or Bidirectional relationships

An important concept to understand when modelling the relationships of your application is, should the relationship be Unidirectional or Bidirectional?

A Unidirectional relationship is where the relationship flows in one direction, whereas a Bidirectional relationship flows in both directions. It’s important to choose the appropriate type of relationship because it can add unnecessary complexity to your application if you don’t.

The decision of whether to implement a Unidirectional or Bidirectional relationship comes down to how the business rules of your application dictate how you should be able to interact with the objects.

For example, if a user is required to have a token through a One-to-One relationship, but you would never have to get a user from a token, choosing a Unidirectional relationship would be best as it will reduce complexity.

On the other hand, if you do require the ability to get the user from the token object, the relationship would need to be bidirectional.

Understanding the Owning Side and the Inverse Side

Another important concept of Object Relational Mapping is the Owning Side and the Inverse Side of a relationship. This is important because it will dictate how Doctrine will persist your entities to the database.

For example, imagine we had a Many-to-Many bidirectional relationship between users and groups. What would happen in the following scenario?

// Select three Users from the database
$user1 = EntityManager::find("User", 1);
$user2 = EntityManager::find("User", 2);
$user3 = EntityManager::find("User", 3);

// Select a Group from the database
$group = EntityManager::find("Group", 1);

// Add the first group to the user
$user1->addToGroup($group);
EntityManager::persist($user1);

// Add the second and third users to the group
$group->addUser($user2);
$group->addUser($user3);
EntityManager::persist($group);

EntityManager::flush();

You might expect that the ORM would know to add all three users to the group, but that is not the case.

In a bidirectional relationship, there are two references to the association between users and groups because we can access the relationship on both sides. This means the relationship exists in two places in the memory of the ORM.

As you can see in the example above, we can add a group to a user or add users to a group. This means that the two references to the association can change independently of each other.

When it comes to persisting the association to the database, Doctrine has two references to the same association that are different, and so it has to choose which one is correct in order to update the database.

In order to solve this problem, we must define the Owning Side of the relationship. A Unidirectional relationship will have an Owning Side, whilst a Bidirectional relationship will have an Owning Side and an Inverse Side.

When Doctrine comes to update the association, it will look at the Owning Side of the relationship to determine if anything has changed.

Setting the Owning and Inverse Sides of a relationship can be achieved by defining it in the annotation of the relationship in the entity. We’ll look at how to do this later in this tutorial.

For Many-to-Many bidirectional relationships, the Owning Side can be on either side. For One-to-One bidirectional relationships, the Owning Side is the side with the foreign key. For One-to-Many or Many-to-One bidirectional relationships the Many side of the relationship must be the owning side.

However, the Owning Side and the Inverse Side of the relationship are technical aspects of how the ORM works, and have nothing to do with the business rules of your application. You might have a relationship where you consider the owning side to be one entity, but the database considers the owning side to be the other entity. These two ideas are unrelated and so how you implement the business rules of your application should not be dictated by your ORM.

Now it has to be said that you would typically not have code like the example above in your application. It is your responsibility to ensure that you are associating objects correctly. This means you should update both sides of the association, or only the owning side. Any changes made only to the inverse side will be ignored.

Using Collections

If you are already familiar with Laravel’s Eloquent, you will know that when you retrieve many items from the database you will be returned an instance of Collection, which inherits from Illuminate\Support\Collection.

You can think of a Collection instance as an array on steroids. This is because you can iterate over a collection to get each item like you would with an array, but the Collection object has a number of useful methods that make working with many items much easier.

When representing relationships in Doctrine 2, you will use an instance of ArrayCollection.

For example:

use Doctrine\Common\Collections\ArrayCollection;

/** @Entity **/
class User
{
    public function __construct()
    {
        $this->cribbbs = new ArrayCollection();
    }
}

Doctrine’s ArrayCollection is another plain old PHP object, but it has a number of methods and properties that make working with a collection of items easier. The ArrayCollection has no dependencies and so you can think of it as an enhancement of PHP, rather than a part of Doctrine itself.

As you can see in the example above, we instantiate a new ArrayCollection instance within the __construct() method of the entity.

Defining relationships

As you can probably imagine, there are many different types of relationships that can be defined using Doctrine 2. I won’t be covering how to implement every type of relationship because it would be out of the scope of this tutorial. Instead I’ll only be looking at defining the relationship between users and cribbbs in my application. In future tutorials I will be covering implementing each different type of relationship as I encounter the requirement.

If you want to learn more about every different type of relationship in Doctrine 2, I would take a look at the documentation.

For my application I want to create a Many-to-Many relationship between Users and Cribbbs (You can think of a Cribbb as just another name for a group). So in my application many Users are in many Cribbbs and many Cribbbs have many Users.

This relationship will be Bidirectional because I want to be able to access the relationship on either side. The Owning Side of the relationship will be on the Cribbb side, however I do want to be able to update the relationship on the User side too. This means I will need to write code to keep both sides of the relationship in sync.

Here’s how I would implement this relationship in my Doctrine entities.

Note: I’m leaving out certain details of the entities for clarity and so the following code is closer to pseudo code. In the coming weeks you will see how this relationship is implemented in the context of the application.

Firstly I will define the Cribbb entity:

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * @ORM\Entity
 * @ORM\Table(name="cribbbs")
 */
class Cribbb
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToMany(targetEntity="User", inversedBy="cribbbs")
     */
    private $users;

    public function __construct()
    {
        $this->users = new ArrayCollection();
    }

    /**
     * Add a User to the Cribbb
     *
     * @param User $user
     * @return void
     */
    public function addUser(User $user)
    {
        $this->users[] = $user;
    }
}

If you have read last week’s tutorial a lot of this code should be familiar.

Next I will define the User entity:

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * @ORM\Entity
 * @ORM\Table(name="users")
 */
class User
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToMany(targetEntity="Cribbb", mappedBy="users")
     */
    private $cribbbs;

    public function __construct()
    {
        $this->cribbbs = new ArrayCollection();
    }

    /**
     * Add the user to a Cribbb
     *
     * @param Cribbb $cribbb
     * @return void
     */
    public function addToCribbb(Cribbb $cribbb)
    {
        $this->cribbbs[] = $cribbb;
    }
}

There’s a couple of things to notice in these two entities.

Firstly I’ve instantiated a new ArrayCollection for the relationship within each __construct() method.

Next I’ve defined the relationship in both entities. On the Cribbb side I’ve got:

/**
 * @ORM\ManyToMany(targetEntity="User", inversedBy="cribbbs")
 */
private $users;

And on the User side I’ve got:

/**
 * @ORM\ManyToMany(targetEntity="Cribbb", mappedBy="users")
 */
private $cribbbs;

This is defining a bidirectional Many-to-Many relationship with the Cribbb entity as the Owning Side.

If I wanted a Unidirectional relationship I would only need to define the relationship on the Owning entity (in this case the Cribbb entity).

You can tell that the Cribbb entity is the Owning Side because the User side of the relationship is “mapped by” the other side of the relationship.

I’ve also added methods on both entities to add an entity to the relationship on either side. However as I mentioned above, if I tried to add a Cribbb to the User entity Doctrine will ignore this association when it came to persisting it to the database.

To use the entities in my application, I would write something like this:

// Create a new User
$user = new User();
EntityManager::persist($user);

// Create a new Cribbb and add the user
$cribbb = new Cribbb();
$cribbb->addUser($user);
EntityManager::persist($cribbb);

EntityManager::flush();

In this example I’m creating a new user and a new cribbb and I’m adding the user to the cribbb. This will work correctly.

However if I were to try to update the relationship on the other side it would fail:

// Create a new Cribbb
$cribbb = new Cribbb();
EntityManager::persist($cribbb);

// Create a new User and add the Cribbb
$user = new User();
$user->addToCribbb($cribbb);
EntityManager::persist($user);

EntityManager::flush();

The example above would create a new User and a new Cribbb, but it would not save the association to the database because we are creating it on the inverse side.

To fix this problem we need to ensure that the association is in sync on both sides. To do this we can amend the addToCribbb() method on the User entity:

/**
 * Add the user to a Cribbb
 *
 * @param Cribbb $cribbb
 * @return void
 */
public function addToCribbb(Cribbb $cribbb)
{
    $cribbb->addUser($this);

    $this->cribbbs[] = $cribbb;
}

Now whenever I add a Cribbb to a User, both side’s references to the association will be updated.

Ordering Collections

When you have a to-Many relationship, you will often want to order those entities automatically whenever you retrieve them from the database.

Doctrine 2 has a really nice feature where you can specify the order within the relationship annotation:

/** @Entity */
class User
{
    /**
     * @ORM\ManyToMany(targetEntity="Cribbb", mappedBy="users")
     * @ORM\OrderBy({"name" = "ASC"})
     */
    private $cribbbs;
}

Now when you retrieve the cribbbs that a user belongs to, the results will be automatically ordered by their name in ascending order.

Conclusion

Phew, that was a long post! It took me ages to think of how to explain these concepts in easy to understand language, so hopefully this was a good introduction to implementing relationships in Doctrine 2.

Defining the relationships of your entities can be tricky at first because you need to have a good understanding of your business rules and how they can be implemented in code.

Implementing relationships in Doctrine 2 can seem pretty complicated, but it is actually not that difficult once you familiarise yourself with how things work. Doctrine 2 is a powerful ORM and so it makes sense that it gives you this granular control over how your relationships work.

This is a series of posts on building an entire Open Source application called Cribbb. All of the tutorials will be free to web, and all of the code is available on GitHub.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.