
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.
What Exactly Are Doctrine Relations?
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:
- OneToOne
- OneToMany
- ManyToOne
- ManyToMany
Each relationship type has distinct characteristics and use cases that we’ll explore in detail.
Setting Up Your Environment
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/orm
Code 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 Relations in Doctrine
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 Relations
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
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.
Cascade Operations
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 operations
For 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)
Fetch Types: Lazy, Eager, and Extra Lazy
Doctrine provides different fetch types to control when related entities are loaded from the database:
- Lazy (default): Related entities are loaded only when they are accessed
- Eager: Related entities are loaded immediately when the owning entity is loaded
- Extra Lazy: Only the count of related entities is loaded, and individual entities are loaded only when needed
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.
Self-Referencing Relations
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)
Bidirectional vs. Unidirectional Relations
In Doctrine, relations can be bidirectional or unidirectional:
- Bidirectional: Both entities have references to each other
- Unidirectional: Only one entity has a reference to the other
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)
Best Practices for Doctrine Relations
Here are some key best practices I’ve learned from working with Doctrine relations:
- Always update both sides of bidirectional relationships. Create helper methods like
addPost()
andremovePost()
that handle this for you. - Initialize collections in constructors. This prevents null reference errors.
- Consider performance implications. Large collections can cause memory issues, so use pagination or extra lazy loading when appropriate.
- Use cascade operations carefully. Think about what should happen to related entities when the parent entity is modified.
- Be mindful of orphan removal. The
orphanRemoval=true
option will delete related entities when they’re removed from a collection.
Conclusion
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!
Discover more from CodeSamplez.com
Subscribe to get the latest posts sent to your email.
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