How to Work with Doctrine Associations / Relations

3.3 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
     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;
    }
    
  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    # src/AppBundle/Resources/config/doctrine/Product.orm.yml
    AppBundle\Entity\Product:
        type: entity
        # ...
        manyToOne:
            category:
                targetEntity: Category
                inversedBy: products
                joinColumn:
                    name: category_id
                    referencedColumnName: id
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- src/AppBundle/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="AppBundle\Entity\Product">
            <!-- ... -->
            <many-to-one
                field="category"
                target-entity="Category"
                inversed-by="products"
                join-column="category">
    
                <join-column name="category_id" referenced-column-name="id" />
            </many-to-one>
        </entity>
    </doctrine-mapping>
    

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
     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();
        }
    }
    
  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    # src/AppBundle/Resources/config/doctrine/Category.orm.yml
    AppBundle\Entity\Category:
        type: entity
        # ...
        oneToMany:
            products:
                targetEntity: 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/AppBundle/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="AppBundle\Entity\Category">
            <!-- ... -->
            <one-to-many
                field="products"
                target-entity="Product"
                mapped-by="category" />
    
            <!--
                don't forget to init the collection in
                the __construct() method of the entity
            -->
        </entity>
    </doctrine-mapping>
    

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.

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.

../_images/mapping_relations.png

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

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.