How to Work with Doctrine Associations / Relations

Version: 3.3
Edit this page

Warning: You are browsing the documentation for Symfony 3.3, which is no longer maintained.

Read the updated version of this page for Symfony 5.3 (the current stable version).

How to Work with Doctrine Associations / Relations

Suppose that each product in your application belongs to exactly one category. In this case, you'll need a Category class, and a way to relate a Product object to a Category object.

Start by creating the Category entity. Since you know that you'll eventually need to persist category objects through Doctrine, you can let Doctrine create the class for you.

1
2
3
$ php bin/console doctrine:generate:entity --no-interaction \
    --entity="AppBundle:Category" \
    --fields="name:string(255)"

This command generates the Category entity for you, with an id field, a name field and the associated getter and setter functions.

Relationship Mapping Metadata

In this example, each category can be associated with many products, while each product can be associated with only one category. This relationship can be summarized as: many products to one category (or equivalently, one category to many products).

From the perspective of the Product entity, this is a many-to-one relationship. From the perspective of the Category entity, this is a one-to-many relationship. This is important, because the relative nature of the relationship determines which mapping metadata to use. It also determines which class must hold a reference to the other class.

To relate the Product and Category entities, simply create a category property on the Product class, annotated as follows:

  • Annotations
  • YAML
  • XML
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/AppBundle/Entity/Product.php

// ...
class Product
{
    // ...

    /**
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     */
    private $category;
}

This many-to-one mapping is critical. It tells Doctrine to use the category_id column on the product table to relate each record in that table with a record in the category table.

Next, since a single Category object will relate to many Product objects, a products property can be added to the Category class to hold those associated objects.

  • Annotations
  • YAML
  • XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/AppBundle/Entity/Category.php

// ...
use Doctrine\Common\Collections\ArrayCollection;

class Category
{
    // ...

    /**
     * @ORM\OneToMany(targetEntity="Product", mappedBy="category")
     */
    private $products;

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

While the many-to-one mapping shown earlier was mandatory, this one-to-many mapping is optional. It is included here to help demonstrate Doctrine's range of relationship management capabilities. Plus, in the context of this application, it will likely be convenient for each Category object to automatically own a collection of its related Product objects.

Note

The code in the constructor is important. Rather than being instantiated as a traditional array, the $products property must be of a type that implements Doctrine's Collection interface. In this case, an ArrayCollection object is used. This object looks and acts almost exactly like an array, but has some added flexibility. If this makes you uncomfortable, don't worry. Just imagine that it's an array and you'll be in good shape.

See also

To understand inversedBy and mappedBy usage, see Doctrine's Association Updates documentation.

Tip

The targetEntity value in the metadata used above can reference any entity with a valid namespace, not just entities defined in the same namespace. To relate to an entity defined in a different class or bundle, enter a full namespace as the targetEntity.

Now that you've added new properties to both the Product and Category classes, you must generate the missing getter and setter methods manually or using your own IDE.

Ignore the Doctrine metadata for a moment. You now have two classes - Product and Category, with a natural many-to-one relationship. The Product class holds a single Category object, and the Category class holds a collection of Product objects. In other words, you've built your classes in a way that makes sense for your application. The fact that the data needs to be persisted to a database is always secondary.

Now, review the metadata above the Product entity's $category property. It tells Doctrine that the related class is Category, and that the id of the related category record should be stored in a category_id field on the product table.

In other words, the related Category object will be stored in the $category property, but behind the scenes, Doctrine will persist this relationship by storing the category's id in the category_id column of the product table.

The metadata above the Category entity's $products property is less complicated. It simply tells Doctrine to look at the Product.category property to figure out how the relationship is mapped.

Before you continue, be sure to tell Doctrine to add the new category table, the new product.category_id column, and the new foreign key:

1
$ php bin/console doctrine:schema:update --force

Now you can see this new code in action! Imagine you're inside a controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ...

use AppBundle\Entity\Category;
use AppBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    public function createProductAction()
    {
        $category = new Category();
        $category->setName('Computer Peripherals');

        $product = new Product();
        $product->setName('Keyboard');
        $product->setPrice(19.99);
        $product->setDescription('Ergonomic and stylish!');

        // relate this product to the category
        $product->setCategory($category);

        $em = $this->getDoctrine()->getManager();
        $em->persist($category);
        $em->persist($product);
        $em->flush();

        return new Response(
            'Saved new product with id: '.$product->getId()
            .' and new category with id: '.$category->getId()
        );
    }
}

Now, a single row is added to both the category and product tables. The product.category_id column for the new product is set to whatever the id is of the new category. Doctrine manages the persistence of this relationship for you.

When you need to fetch associated objects, your workflow looks just like it did before. First, fetch a $product object and then access its related Category object:

1
2
3
4
5
6
7
8
9
10
11
12
13
use AppBundle\Entity\Product;
// ...

public function showAction($productId)
{
    $product = $this->getDoctrine()
        ->getRepository(Product::class)
        ->find($productId);

    $categoryName = $product->getCategory()->getName();

    // ...
}

In this example, you first query for a Product object based on the product's id. This issues a query for just the product data and hydrates the $product object with that data. Later, when you call $product->getCategory()->getName(), Doctrine silently makes a second query to find the Category that's related to this Product. It prepares the $category object and returns it to you.

What's important is the fact that you have easy access to the product's related category, but the category data isn't actually retrieved until you ask for the category (i.e. it's "lazily loaded").

You can also query in the other direction:

1
2
3
4
5
6
7
8
9
10
public function showProductsAction($categoryId)
{
    $category = $this->getDoctrine()
        ->getRepository(Category::class)
        ->find($categoryId);

    $products = $category->getProducts();

    // ...
}

In this case, the same things occur: you first query out for a single Category object, and then Doctrine makes a second query to retrieve the related Product objects, but only once/if you ask for them (i.e. when you call getProducts()). The $products variable is an array of all Product objects that relate to the given Category object via their category_id value.

This "lazy loading" is possible because, when necessary, Doctrine returns a "proxy" object in place of the true object. Look again at the above example:

1
2
3
4
5
6
7
8
9
$product = $this->getDoctrine()
    ->getRepository(Product::class)
    ->find($productId);

$category = $product->getCategory();

// prints "Proxies\AppBundleEntityCategoryProxy"
dump(get_class($category));
die();

This proxy object extends the true Category object, and looks and acts exactly like it. The difference is that, by using a proxy object, Doctrine can delay querying for the real Category data until you actually need that data (e.g. until you call $category->getName()).

The proxy classes are generated by Doctrine and stored in the cache directory. And though you'll probably never even notice that your $category object is actually a proxy object, it's important to keep it in mind.

In the next section, when you retrieve the product and category data all at once (via a join), Doctrine will return the true Category object, since nothing needs to be lazily loaded.

In the above examples, two queries were made - one for the original object (e.g. a Category) and one for the related object(s) (e.g. the Product objects).

Tip

Remember that you can see all of the queries made during a request via the web debug toolbar.

Of course, if you know up front that you'll need to access both objects, you can avoid the second query by issuing a join in the original query. Add the following method to the ProductRepository class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/AppBundle/Repository/ProductRepository.php
public function findOneByIdJoinedToCategory($productId)
{
    $query = $this->getEntityManager()
        ->createQuery(
        'SELECT p, c FROM AppBundle:Product p
        JOIN p.category c
        WHERE p.id = :id'
    )->setParameter('id', $productId);

    try {
        return $query->getSingleResult();
    } catch (\Doctrine\ORM\NoResultException $e) {
        return null;
    }
}

Now, you can use this method in your controller to query for a Product object and its related Category with just one query:

1
2
3
4
5
6
7
8
9
10
public function showAction($productId)
{
    $product = $this->getDoctrine()
        ->getRepository(Product::class)
        ->findOneByIdJoinedToCategory($productId);

    $category = $product->getCategory();

    // ...
}

More Information on Associations

This section has been an introduction to one common type of entity relationship, the one-to-many relationship. For more advanced details and examples of how to use other types of relations (e.g. one-to-one, many-to-many), see Doctrine's Association Mapping Documentation.

Note

If you're using annotations, you'll need to prepend all annotations with @ORM\ (e.g. @ORM\OneToMany), which is not reflected in Doctrine's documentation. You'll also need to include the use Doctrine\ORM\Mapping as ORM; statement, which imports the ORM annotations prefix.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.