When I first started working with Doctrine ORM in my PHP projects, I found the concept of entity relationships pretty confusing. There were so many annotations, configuration options, and terminology that felt overwhelming. But after years of building applications with Doctrine, I’ve come to appreciate how powerful these relationship mappings really are.
In this guide, I’ll walk you through everything you need to know about Doctrine relations. We’ll cover the fundamentals, explore different relationship types, and look at real-world code examples. Trust me, once you understand how to properly set up your entity relationships, your data modeling will become WAY more intuitive.
Doctrine relations define how different entities in your database connect to each other. These relationships mirror the associations between your database tables, but with the added benefit of object-oriented programming principles.
Doctrine supports four main types of entity relationships:
Each relationship type has distinct characteristics and use cases that we’ll explore in detail.
Before diving into relations, make sure you have Doctrine properly installed and configured. If you’re using Symfony, you’ll already have Doctrine integrated. Otherwise, you can install it using Composer:
composer require doctrine/ormCode language: JavaScript (javascript) Your entity classes should extend the Doctrine Entity class and use annotations (or attributes in newer PHP versions) to define mappings.
Tip 💡: Completely new to Doctrine World? PHP Doctrine Guide For Beginners might be just what you need!
OneToOne relationships are the simplest to understand. They connect one entity to exactly one other entity. This is like saying a Person has one Profile, or a User has one Address.
Let’s look at a practical example:
<?php
// src/Entity/User.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="users")
*/
class User
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=100)
*/
private $username;
/**
* @ORM\OneToOne(targetEntity="Profile", inversedBy="user", cascade={"persist", "remove"})
* @ORM\JoinColumn(name="profile_id", referencedColumnName="id")
*/
private $profile;
// Getters and setters...
}Code language: HTML, XML (xml) And the related Profile entity:
<?php
// src/Entity/Profile.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="profiles")
*/
class Profile
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $fullName;
/**
* @ORM\OneToOne(targetEntity="User", mappedBy="profile")
*/
private $user;
// Getters and setters...
}Code language: HTML, XML (xml) Notice the mappedBy and inversedBy attributes? These tell Doctrine which side of the relationship is the owning side. In OneToOne relations, the entity with the inversedBy attribute (User in this case) is the owning side, while the entity with the mappedBy attribute (Profile) is the inverse side.
The owning side always contains the foreign key in the database, so in this example, the users table will have a profile_id column.
OneToMany and ManyToOne are actually two sides of the same relationship. When you define a OneToMany relation in one entity, you automatically define a ManyToOne relation in the other entity.
A perfect example would be a Blog and its Posts. A Blog can have many Posts, but each Post belongs to exactly one Blog.
Here’s how to implement this:
<?php
// src/Entity/Blog.php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="blogs")
*/
class Blog
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=100)
*/
private $name;
/**
* @ORM\OneToMany(targetEntity="Post", mappedBy="blog", cascade={"persist", "remove"})
*/
private $posts;
public function __construct()
{
$this->posts = new ArrayCollection();
}
public function getPosts(): Collection
{
return $this->posts;
}
public function addPost(Post $post): self
{
if (!$this->posts->contains($post)) {
$this->posts[] = $post;
$post->setBlog($this);
}
return $this;
}
// Other getters and setters...
}Code language: HTML, XML (xml) And the Post entity:
<?php
// src/Entity/Post.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="posts")
*/
class Post
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $title;
/**
* @ORM\ManyToOne(targetEntity="Blog", inversedBy="posts")
* @ORM\JoinColumn(name="blog_id", referencedColumnName="id")
*/
private $blog;
public function getBlog(): ?Blog
{
return $this->blog;
}
public function setBlog(?Blog $blog): self
{
$this->blog = $blog;
return $this;
}
// Other getters and setters...
}Code language: HTML, XML (xml) In this case, the Post entity has the inversedBy attribute and is therefore the owning side of the relationship. The posts table will have a blog_id column.
One important thing to remember is that when you’re working with collections, you should always initialize them in the constructor to avoid null reference errors.
ManyToMany relations are a bit more complex because they require a join table in the database. A classic example is the relationship between Products and Categories – a Product can belong to many Categories, and a Category can contain many Products.
Here’s how to implement a ManyToMany relation:
<?php
// src/Entity/Product.php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="products")
*/
class Product
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=100)
*/
private $name;
/**
* @ORM\ManyToMany(targetEntity="Category", inversedBy="products")
* @ORM\JoinTable(name="products_categories",
* joinColumns={@ORM\JoinColumn(name="product_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="category_id", referencedColumnName="id")}
* )
*/
private $categories;
public function __construct()
{
$this->categories = new ArrayCollection();
}
public function getCategories(): Collection
{
return $this->categories;
}
public function addCategory(Category $category): self
{
if (!$this->categories->contains($category)) {
$this->categories[] = $category;
$category->addProduct($this);
}
return $this;
}
public function removeCategory(Category $category): self
{
if ($this->categories->contains($category)) {
$this->categories->removeElement($category);
$category->removeProduct($this);
}
return $this;
}
// Other getters and setters...
}Code language: HTML, XML (xml) And the Category entity:
<?php
// src/Entity/Category.php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="categories")
*/
class Category
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=100)
*/
private $name;
/**
* @ORM\ManyToMany(targetEntity="Product", mappedBy="categories")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
public function getProducts(): Collection
{
return $this->products;
}
public function addProduct(Product $product): self
{
if (!$this->products->contains($product)) {
$this->products[] = $product;
}
return $this;
}
public function removeProduct(Product $product): self
{
if ($this->products->contains($product)) {
$this->products->removeElement($product);
}
return $this;
}
// Other getters and setters...
}Code language: HTML, XML (xml) In ManyToMany relations, you have to specify a join table with the @ORM\JoinTable annotation. This table will contain foreign keys to both entities.
One of the most powerful features of Doctrine relations is the ability to cascade operations. When you perform an operation on one entity, you can automatically apply the same operation to related entities.
The cascade option supports the following values:
persist: When you persist the owning entity, related entities are also persistedremove: When you remove the owning entity, related entities are also removedmerge: When you merge the owning entity, related entities are also mergeddetach: When you detach the owning entity, related entities are also detachedall: Applies all cascade operationsFor example, if you want to automatically delete all Posts when a Blog is deleted, you would use the cascade={"remove"} option:
/**
* @ORM\OneToMany(targetEntity="Post", mappedBy="blog", cascade={"persist", "remove"})
*/
private $posts;Code language: PHP (php) Doctrine provides different fetch types to control when related entities are loaded from the database:
You can specify the fetch type using the fetch option:
/**
* @ORM\OneToMany(targetEntity="Post", mappedBy="blog", fetch="EAGER")
*/
private $posts;Code language: PHP (php) I usually recommend sticking with lazy loading unless you have a specific reason to use eager loading, as it can significantly impact performance with large datasets.
Sometimes you need entities that reference themselves, like in a hierarchical structure. For example, a Category might have child Categories, or an Employee might have a Manager who is also an Employee.
Here’s how to implement a self-referencing relation:
<?php
// src/Entity/Category.php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="categories")
*/
class Category
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=100)
*/
private $name;
/**
* @ORM\ManyToOne(targetEntity="Category", inversedBy="children")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
*/
private $parent;
/**
* @ORM\OneToMany(targetEntity="Category", mappedBy="parent")
*/
private $children;
public function __construct()
{
$this->children = new ArrayCollection();
}
// Getters and setters...
}Code language: HTML, XML (xml) In Doctrine, relations can be bidirectional or unidirectional:
Most of the examples I’ve shown so far are bidirectional. For a unidirectional relation, you would simply omit the inversedBy or mappedBy attribute:
/**
* @ORM\ManyToOne(targetEntity="Blog")
* @ORM\JoinColumn(name="blog_id", referencedColumnName="id")
*/
private $blog;Code language: PHP (php) Here are some key best practices I’ve learned from working with Doctrine relations:
addPost() and removePost() that handle this for you.orphanRemoval=true option will delete related entities when they’re removed from a collection.Mastering Doctrine relations is essential for building robust PHP applications. We’ve covered the basics of OneToOne, OneToMany, ManyToOne, and ManyToMany relationships, along with important concepts like cascade operations and fetch types.
Remember that good entity design mirrors your business domain. Take time to think about the relationships between your entities and how they map to the real-world concepts they represent.
If you have any questions about implementing Doctrine relations in your projects, feel free to leave a comment below!
Tired of repetitive tasks eating up your time? Python can help you automate the boring stuff — from organizing files to scraping websites and sending…
Learn python file handling from scratch! This comprehensive guide walks you through reading, writing, and managing files in Python with real-world examples, troubleshooting tips, and…
You've conquered the service worker lifecycle, mastered caching strategies, and explored advanced features. Now it's time to lock down your implementation with battle-tested service worker…
This website uses cookies.
View Comments
Nice tutorial! Thank you, great job!
can u tell something about entity owner side and inverse side ?
Hi Thank you for your tutorial.
i have type table that have Many-to-Many relationship with category and many to many relation table (categoryOftype table) have One-to-Many relationship with items table how i do this with entity relationship in doctrine 2 .
tanks