How to Work with Doctrine Associations / Relations

How to Work with Doctrine Associations / Relations

Screencast

Do you prefer video tutorials? Check out the Mastering Doctrine Relations screencast series.

There are two main relationship/association types:

ManyToOne / OneToMany

The most common relationship, mapped in the database with a foreign key column (e.g. a category_id column on the product table). This is actually only 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 with a name field:

  1. $ php bin/console make:entity Category
  2. New property name (press <return> to stop adding fields):
  3. > name
  4. Field type (enter ? to see all types) [string]:
  5. > string
  6. Field length [255]:
  7. > 255
  8. Can this field be null in the database (nullable) (yes/no) [no]:
  9. > no
  10. New property name (press <return> to stop adding fields):
  11. >
  12. (press enter again to finish)

This will generate your new entity class:

  1. // src/Entity/Category.php
  2. namespace App\Entity;
  3. // ...
  4. class Category
  5. {
  6. /**
  7. * @ORM\Id
  8. * @ORM\GeneratedValue
  9. * @ORM\Column(type="integer")
  10. */
  11. private $id;
  12. /**
  13. * @ORM\Column(type="string")
  14. */
  15. private $name;
  16. // ... getters and setters
  17. }

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. You can do this by hand, or by using the make:entity command, which will ask you several questions about your relationship. If you’re not sure of the answer, don’t worry! You can always change the settings later:

  1. $ php bin/console make:entity
  2. Class name of the entity to create or update (e.g. BraveChef):
  3. > Product
  4. New property name (press <return> to stop adding fields):
  5. > category
  6. Field type (enter ? to see all types) [string]:
  7. > relation
  8. What class should this entity be related to?:
  9. > Category
  10. Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
  11. > ManyToOne
  12. Is the Product.category property allowed to be null (nullable)? (yes/no) [yes]:
  13. > no
  14. Do you want to add a new property to Category so that you can access/update
  15. Product objects from it - e.g. $category->getProducts()? (yes/no) [yes]:
  16. > yes
  17. New field name inside Category [products]:
  18. > products
  19. Do you want to automatically delete orphaned App\Entity\Product objects
  20. (orphanRemoval)? (yes/no) [no]:
  21. > no
  22. New property name (press <return> to stop adding fields):
  23. >
  24. (press enter again to finish)

This made changes to two entities. First, it added a new category property to the Product entity (and getter & setter methods):

  • Annotations

    1. // src/Entity/Product.php
    2. namespace App\Entity;
    3. // ...
    4. class Product
    5. {
    6. // ...
    7. /**
    8. * @ORM\ManyToOne(targetEntity="App\Entity\Category", inversedBy="products")
    9. */
    10. private $category;
    11. public function getCategory(): ?Category
    12. {
    13. return $this->category;
    14. }
    15. public function setCategory(?Category $category): self
    16. {
    17. $this->category = $category;
    18. return $this;
    19. }
    20. }
  • YAML

    1. # src/Resources/config/doctrine/Product.orm.yml
    2. App\Entity\Product:
    3. type: entity
    4. # ...
    5. manyToOne:
    6. category:
    7. targetEntity: App\Entity\Category
    8. inversedBy: products
    9. joinColumn:
    10. nullable: false
  • XML

    1. <!-- src/Resources/config/doctrine/Product.orm.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
    6. https://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    7. <entity name="App\Entity\Product">
    8. <!-- ... -->
    9. <many-to-one
    10. field="category"
    11. target-entity="App\Entity\Category"
    12. inversed-by="products">
    13. <join-column nullable="false"/>
    14. </many-to-one>
    15. </entity>
    16. </doctrine-mapping>

This ManyToOne 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 one Category object will relate to many Product objects, the make:entity command also added a products property to the Category class that will hold these objects:

  • Annotations

    1. // src/Entity/Category.php
    2. namespace App\Entity;
    3. // ...
    4. use Doctrine\Common\Collections\ArrayCollection;
    5. use Doctrine\Common\Collections\Collection;
    6. class Category
    7. {
    8. // ...
    9. /**
    10. * @ORM\OneToMany(targetEntity="App\Entity\Product", mappedBy="category")
    11. */
    12. private $products;
    13. public function __construct()
    14. {
    15. $this->products = new ArrayCollection();
    16. }
    17. /**
    18. * @return Collection|Product[]
    19. */
    20. public function getProducts(): Collection
    21. {
    22. return $this->products;
    23. }
    24. // addProduct() and removeProduct() were also added
    25. }
  • YAML

    1. # src/Resources/config/doctrine/Category.orm.yml
    2. App\Entity\Category:
    3. type: entity
    4. # ...
    5. oneToMany:
    6. products:
    7. targetEntity: App\Entity\Product
    8. mappedBy: category
    9. # Don't forget to initialize the collection in
    10. # the __construct() method of the entity
  • XML

    1. <!-- src/Resources/config/doctrine/Category.orm.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
    6. https://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    7. <entity name="App\Entity\Category">
    8. <!-- ... -->
    9. <one-to-many
    10. field="products"
    11. target-entity="App\Entity\Product"
    12. mapped-by="category"/>
    13. <!--
    14. don't forget to init the collection in
    15. the __construct() method of the entity
    16. -->
    17. </entity>
    18. </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 (this is one of the questions make:entity asks you). 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.

What is the ArrayCollection Stuff?

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 is an array and you’ll be in good shape.

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

  1. $ php bin/console doctrine:migrations:diff
  2. $ 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!

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

  1. // src/Controller/ProductController.php
  2. namespace App\Controller;
  3. // ...
  4. use App\Entity\Category;
  5. use App\Entity\Product;
  6. use Symfony\Component\HttpFoundation\Response;
  7. class ProductController extends AbstractController
  8. {
  9. /**
  10. * @Route("/product", name="product")
  11. */
  12. public function index(): Response
  13. {
  14. $category = new Category();
  15. $category->setName('Computer Peripherals');
  16. $product = new Product();
  17. $product->setName('Keyboard');
  18. $product->setPrice(19.99);
  19. $product->setDescription('Ergonomic and stylish!');
  20. // relates this product to the category
  21. $product->setCategory($category);
  22. $entityManager = $this->getDoctrine()->getManager();
  23. $entityManager->persist($category);
  24. $entityManager->persist($product);
  25. $entityManager->flush();
  26. return new Response(
  27. 'Saved new product with id: '.$product->getId()
  28. .' and new category with id: '.$category->getId()
  29. );
  30. }
  31. }

When you go to /product, 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:

../_images/mapping_relations.png

If you’re new to an ORM, this is the hardest concept: you need to stop thinking about your database, and instead only think about your objects. Instead of setting the category’s integer id onto Product, you set the entire Category object. Doctrine takes care of the rest when saving.

Updating the Relationship from the Inverse Side

Could you also call $category->addProduct() to change the relationship? Yes, but, only because the make:entity command helped us. For more details, see: associations-inverse-side.

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

  1. // src/Controller/ProductController.php
  2. namespace App\Controller;
  3. use App\Entity\Product;
  4. // ...
  5. class ProductController extends AbstractController
  6. {
  7. public function show(int $id): Response
  8. {
  9. $product = $this->getDoctrine()
  10. ->getRepository(Product::class)
  11. ->find($id);
  12. // ...
  13. $categoryName = $product->getCategory()->getName();
  14. // ...
  15. }
  16. }

In this example, you first query for a Product object based on the product’s id. This issues a query to fetch only the product data and hydrates the $product. 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.

../_images/mapping_relations_proxy.png

What’s important is the fact that you have 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”).

Because we mapped the optional OneToMany side, you can also query in the other direction:

  1. // src/Controller/ProductController.php
  2. // ...
  3. class ProductController extends AbstractController
  4. {
  5. public function showProducts(int $id): Response
  6. {
  7. $category = $this->getDoctrine()
  8. ->getRepository(Category::class)
  9. ->find($id);
  10. $products = $category->getProducts();
  11. // ...
  12. }
  13. }

In this case, the same things occur: you first query for a single Category object. Then, only when (and if) you access the products, Doctrine makes a second query to retrieve the related Product objects. This extra query can be avoided by adding JOINs.

Relationships and Proxy Classes

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. $product = $this->getDoctrine()
  2. ->getRepository(Product::class)
  3. ->find($id);
  4. $category = $product->getCategory();
  5. // prints "Proxies\AppEntityCategoryProxy"
  6. dump(get_class($category));
  7. 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. You’ll probably never even notice that your $category object is actually a proxy object.

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 examples above, 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.

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. // src/Repository/ProductRepository.php
  2. // ...
  3. class ProductRepository extends ServiceEntityRepository
  4. {
  5. public function findOneByIdJoinedToCategory(int $productId): ?Product
  6. {
  7. $entityManager = $this->getEntityManager();
  8. $query = $entityManager->createQuery(
  9. 'SELECT p, c
  10. FROM App\Entity\Product p
  11. INNER JOIN p.category c
  12. WHERE p.id = :id'
  13. )->setParameter('id', $productId);
  14. return $query->getOneOrNullResult();
  15. }
  16. }

This will still return an array of Product objects. But now, when you call $product->getCategory() and use that data, no second query is made.

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

  1. // src/Controller/ProductController.php
  2. // ...
  3. class ProductController extends AbstractController
  4. {
  5. public function show(int $id): Response
  6. {
  7. $product = $this->getDoctrine()
  8. ->getRepository(Product::class)
  9. ->findOneByIdJoinedToCategory($id);
  10. $category = $product->getCategory();
  11. // ...
  12. }
  13. }

Setting Information from the Inverse Side

So far, you’ve updated the relationship by calling $product->setCategory($category). This is no accident! Each relationship has two sides: in this example, Product.category is the owning side and Category.products is the inverse side.

To update a relationship in the database, 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->addProduct() or $category->removeProduct() to update the database? Actually, it is possible, thanks to some clever code that the make:entity command generated:

  1. // src/Entity/Category.php
  2. // ...
  3. class Category
  4. {
  5. // ...
  6. public function addProduct(Product $product): self
  7. {
  8. if (!$this->products->contains($product)) {
  9. $this->products[] = $product;
  10. $product->setCategory($this);
  11. }
  12. return $this;
  13. }
  14. }

The key is $product->setCategory($this), which sets the owning side. Thanks, to this, when you save, the relationship will update in the database.

What about removing a Product from a Category? The make:entity command also generated a removeProduct() method:

  1. // src/Entity/Category.php
  2. namespace App\Entity;
  3. // ...
  4. class Category
  5. {
  6. // ...
  7. public function removeProduct(Product $product): self
  8. {
  9. if ($this->products->contains($product)) {
  10. $this->products->removeElement($product);
  11. // set the owning side to null (unless already changed)
  12. if ($product->getCategory() === $this) {
  13. $product->setCategory(null);
  14. }
  15. }
  16. return $this;
  17. }
  18. }

Thanks to this, 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. // src/Entity/Category.php
  2. // ...
  3. /**
  4. * @ORM\OneToMany(targetEntity="App\Entity\Product", mappedBy="category", orphanRemoval=true)
  5. */
  6. 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.