Doctrine Relations — Complete Guide to Association Mapping
In this tutorial, you will learn about Doctrine Relations. We cover key concepts, practical examples, and best practices to help you master this topic.
Doctrine relations define how entities associate with each other using OneToMany, ManyToOne, ManyToMany, and OneToOne mapping with cascade, orphan removal, and fetch mode configuration.
What You'll Learn
By the end of this tutorial, you'll map all Doctrine relationship types, configure cascade operations, handle orphan removal, choose fetch modes, and design effective association structures.
Why Relations Matter
Entity relationships are the core of any domain model. Proper relation mapping ensures data integrity, efficient queries, and clean object-oriented code.
Real-World Use
An order management system uses ManyToOne from Order to Customer, OneToMany from Order to OrderItem, ManyToMany from Product to Category, and OneToOne from User to Profile.
Relations Path
flowchart LR
A[Doctrine Entities] --> B[Doctrine Relations]
B --> C[OneToMany]
B --> D[ManyToOne]
B --> E[ManyToMany]
B --> F[OneToOne]
B --> G{You Are Here}
style G fill:#f90,color:#fff
ManyToOne Association
The owning side of a bidirectional association.
<?php
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Order {
#[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column]
private int|null $id = null;
#[ORM\ManyToOne(targetEntity: Customer::class, inversedBy: "orders")]
#[ORM\JoinColumn(nullable: false)]
private Customer $customer;
#[ORM\Column(type: "decimal", precision: 10, scale: 2)]
private float $total;
}
#[ORM\Entity]
class Customer {
#[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column]
private int|null $id = null;
#[ORM\OneToMany(mappedBy: "customer", targetEntity: Order::class)]
private Collection $orders;
}
OneToMany Association
The inverse side of a bidirectional ManyToOne.
<?php
#[ORM\Entity]
class Category {
#[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column]
private int|null $id = null;
#[ORM\OneToMany(mappedBy: "category", targetEntity: Product::class, cascade: ["persist", "remove"], orphanRemoval: true)]
private Collection $products;
public function __construct() {
$this->products = new ArrayCollection();
}
public function addProduct(Product $product): void {
$product->setCategory($this);
$this->products->add($product);
}
}
ManyToMany Association
Entities sharing a many-to-many relationship.
<?php
#[ORM\Entity]
class Student {
#[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column]
private int|null $id = null;
#[ORM\ManyToMany(targetEntity: Course::class, inversedBy: "students")]
#[ORM\JoinTable(name: "student_courses")]
private Collection $courses;
public function __construct() {
$this->courses = new ArrayCollection();
}
}
#[ORM\Entity]
class Course {
#[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column]
private int|null $id = null;
#[ORM\ManyToMany(targetEntity: Student::class, mappedBy: "courses")]
private Collection $students;
}
Cascade Operations
Automatically propagate operations to associated entities.
<?php
#[ORM\Entity]
class BlogPost {
#[ORM\OneToMany(mappedBy: "post", targetEntity: Comment::class, cascade: ["persist", "remove"], orphanRemoval: true)]
private Collection $comments;
#[ORM\ManyToMany(targetEntity: Tag::class, cascade: ["persist"])]
private Collection $tags;
public function removeComment(Comment $comment): void {
$this->comments->removeElement($comment);
}
}
Fetch Modes
Control when related entities are loaded.
<?php
#[ORM\Entity]
class UserProfile {
// Always load the user with the profile
#[ORM\OneToOne(inversedBy: "profile", fetch: "EAGER")]
private User $user;
}
#[ORM\Entity]
class User {
// Load orders only when accessed
#[ORM\OneToMany(mappedBy: "user", targetEntity: Order::class, fetch: "LAZY")]
private Collection $orders;
// Load permissions eagerly
#[ORM\ManyToMany(targetEntity: Role::class, fetch: "EAGER")]
private Collection $roles;
}
Common Mistakes
1. Not Initializing Collections
Entity constructors must initialize Collection properties with ArrayCollection.
2. Owning Side Confusion
Only the owning side (with JoinColumn) persists relationship changes. Changes on the inverse side are ignored.
3. Orphan Removal Without Cascade
orphanRemoval requires cascade: ["remove"] to work correctly.
4. Eager Loading on Both Sides
Eager loading on both sides of an association causes circular fetch. Use lazy on one side.
5. Forgetting to Sync Both Sides
When adding to a bidirectional association, set both sides for consistency.
Practice Questions
1. What is the owning side of a relation?
The side with the JoinColumn that stores the foreign key.
2. What does orphanRemoval do?
Removes entities that are no longer referenced in the collection from the database.
3. What is the difference between LAZY and EAGER fetch?
LAZY loads on access. EAGER loads immediately with the parent entity.
4. How do you map a ManyToMany with extra columns?
Use a join entity with OneToMany on both sides instead of @ManyToMany.
5. Challenge: Create entities for a library with Book, Author, and Publisher relations.
<?php
#[ORM\Entity]
class Book {
#[ORM\ManyToOne(inversedBy: "books")]
private Author $author;
#[ORM\ManyToOne]
private Publisher $publisher;
}
#[ORM\Entity]
class Author {
#[ORM\OneToMany(mappedBy: "author", cascade: ["remove"])]
private Collection $books;
}
#[ORM\Entity]
class Publisher {
#[ORM\OneToMany(mappedBy: "publisher")]
private Collection $books;
}
FAQ
{{< faq "How do I order collection items?" "Use #[OrderBy({\"name\": \"ASC\"})] on the association." >}}Mini Project: E-Commerce Entity Relations
Model e-commerce domain entities with proper relations.
<?php
#[ORM\Entity]
class Order {
#[ORM\OneToMany(mappedBy: "order", targetEntity: OrderItem::class, cascade: ["persist", "remove"], orphanRemoval: true)]
private Collection $items;
#[ORM\ManyToOne(inversedBy: "orders")]
private Customer $customer;
}
#[ORM\Entity]
class OrderItem {
#[ORM\ManyToOne(inversedBy: "orderItems")]
private Order $order;
#[ORM\ManyToOne]
private Product $product;
}
What's Next
Doctrine Migrations Doctrine DQL Doctrine Entities
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro