Symfony 4 was released on November 30th.
Update now to the best Symfony ever!

You are browsing the Symfony 4 documentation, which changes significantly from Symfony 3.x. If your app doesn't use Symfony 4 yet, browse the Symfony 3.4 documentation.

How to Work with Doctrine Associations / Relations

How to Work with Doctrine Associations / Relations

There are two main relationship/association types:

ManyToOne / OneToMany
The most common relationship, mapped in the database with a simple foreign key column (e.g. a category_id column on the product table). This is actually just one association type, but seen from the two different sides of the relation.
ManyToMany
Uses a join table and is needed when both sides of the relationship can have many of the other side (e.g. "students" and "classes": each student is in many classes, and each class has many students).

First, you need to determine which relationship to use. If both sides of the relation will contain many of the other side (e.g. "students" and "classes"), you need a ManyToMany relation. Otherwise, you likely need a ManyToOne.

Tip

There is also a OneToOne relationship (e.g. one User has one Profile and vice versa). In practice, using this is similar to ManyToOne.

The ManyToOne / OneToMany Association

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 a Category entity:

1
$ php bin/console make:entity Category

Then, add a name field to that new Category class:

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

class Category
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string")
     */
    private $name;

    // ... getters and setters
}

Mapping the ManyToOne Relationship

In this example, each category can be associated with many products. But, 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.

To map this, first create a category property on the Product class with the ManyToOne annotation:

  • Annotations
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // src/Entity/Product.php
    
    // ...
    class Product
    {
        // ...
    
        /**
         * @ORM\ManyToOne(targetEntity="App\Entity\Category", inversedBy="products")
         * @ORM\JoinColumn(nullable=true)
         */
        private $category;
    
        public function getCategory(): Category
        {
            return $this->category;
        }
    
        public function setCategory(Category $category)
        {
            $this->category = $category;
        }
    }
    
  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    # src/Resources/config/doctrine/Product.orm.yml
    App\Entity\Product:
        type: entity
        # ...
        manyToOne:
            category:
                targetEntity: App\Entity\Category
                inversedBy: products
                joinColumn:
                    nullable: true
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- src/Resources/config/doctrine/Product.orm.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
            http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    
        <entity name="App\Entity\Product">
            <!-- ... -->
            <many-to-one
                field="category"
                target-entity="App\Entity\Category"
                inversed-by="products">
                <join-column nullable="true" />
            </many-to-one>
        </entity>
    </doctrine-mapping>
    

This many-to-one mapping is required. 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 one Category object will relate to many Product objects, add a products property to Category that will hold those objects:

  • Annotations
     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
    // src/Entity/Category.php
    
    // ...
    use Doctrine\Common\Collections\ArrayCollection;
    use Doctrine\Common\Collections\Collection;
    
    class Category
    {
        // ...
    
        /**
         * @ORM\OneToMany(targetEntity="App\Entity\Product", mappedBy="category")
         */
        private $products;
    
        public function __construct()
        {
            $this->products = new ArrayCollection();
        }
    
        /**
         * @return Collection|Product[]
         */
        public function getProducts()
        {
            return $this->products;
        }
    }
    
  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    # src/Resources/config/doctrine/Category.orm.yml
    App\Entity\Category:
        type: entity
        # ...
        oneToMany:
            products:
                targetEntity: App\Entity\Product
                mappedBy: category
    # Don't forget to initialize the collection in
    # the __construct() method of the entity
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <!-- src/Resources/config/doctrine/Category.orm.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
            http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    
        <entity name="App\Entity\Category">
            <!-- ... -->
            <one-to-many
                field="products"
                target-entity="App\Entity\Product"
                mapped-by="category" />
    
            <!--
                don't forget to init the collection in
                the __construct() method of the entity
            -->
        </entity>
    </doctrine-mapping>
    

The ManyToOne mapping shown earlier is required, But, this OneToMany is optional: only add it if you want to be able to access the products that are related to a category. In this example, it will be useful to be able to call $category->getProducts(). If you don't want it, then you also don't need the inversedBy or mappedBy config.

The code inside __construct() is important: The $products property must be a collection object that implements Doctrine's Collection interface. In this case, an ArrayCollection object is used. This looks and acts almost exactly like an array, but has some added flexibility. Just imagine that it's an array and you'll be in good shape.

Your database is setup! Now, execute the migrations like normal:

1
2
$ php bin/console doctrine:migrations:diff
$ php bin/console doctrine:migrations:migrate

Thanks to the relationship, this creates a category_id foreign key column on the product table. Doctrine is ready to persist our relationship!

Setting Information from the Inverse Side

So far, you've updated the relationship by calling $product->setCategory($category). This is no accident: you must set the relationship on the owning side. The owning side is always where the ManyToOne mapping is set (for a ManyToMany relation, you can choose which side is the owning side).

Does this means it's not possible to call $category->setProducts()? Actually, it is possible, by writing clever methods. First, instead of a setProducts() method, create a addProduct() method:

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

// ...
class Category
{
    // ...

    public function addProduct(Product $product)
    {
        if ($this->products->contains($product)) {
            return;
        }

        $this->products[] = $product;
        // set the *owning* side!
        $product->setCategory($this);
    }
}

That's it! The key is $product->setCategory($this), which sets the owning side. Now, when you save, the relationship will update in the database.

What about removing a Product from a Category? Add a removeProduct() method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// src/Entity/Category.php

// ...
class Category
{
    // ...

    public function removeProduct(Product $product)
    {
        $this->products->removeElement($product);
        // set the owning side to null
        $product->setCategory(null);
    }
}

To make this work, you now need to allow null to be passed to Product::setCategory():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// src/Entity/Product.php

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

-     public function getCategory(): Category
+     public function getCategory(): ?Category
    // ...

-     public function setCategory(Category $category)
+     public function setCategory(Category $category = null)
    {
        $this->category = $category;
    }
}

And that's it! Now, if you call $category->removeProduct($product), the category_id on that Product will be set to null in the database.

But, instead of setting the category_id to null, what if you want the Product to be deleted if it becomes "orphaned" (i.e. without a Category)? To choose that behavior, use the orphanRemoval option inside Category:

1
2
3
4
5
6
7
8
// src/Entity/Category.php

// ...

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

Thanks to this, if the Product is removed from the Category, it will be removed from the database entirely.

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.

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