Skip to content

Doctrine Relations — Complete Guide to Association Mapping

DodaTech Updated 2026-06-28 4 min read

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

What is the difference between cascade persist and remove?

Persist saves related entities automatically. Remove deletes them when the parent is deleted.

Can I have self-referencing relations?

Yes. Use ManyToOne and OneToMany to the same entity for tree structures.

What is the maximum association nesting?

No hard limit, but deep nesting hurts performance. Keep it shallow.

{{< faq "How do I order collection items?" "Use #[OrderBy({\"name\": \"ASC\"})] on the association." >}}
What is a join table?

A table used for ManyToMany associations, storing foreign keys from both sides.

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