Updated June 2026. Tested on Symfony 7.x and Doctrine ORM 3. Part of the Techalyst Symfony series.
Real data is connected: a product belongs to a category, an order has many items, an article has many tags. Doctrine maps these connections with relationship attributes so you can walk from one entity to its related ones as plain object properties. The mapping has one concept that confuses people, the owning side, so let us get that clear while we cover the three relationships you will actually use.
ManyToOne and OneToMany
These two are a pair, and together they model "many of these belong to one of those". Many products belong to one category. Each side is declared on its own entity:
// Product: many products, one category
#[ORM\ManyToOne(inversedBy: 'products')]
private ?Category $category = null;
// Category: one category, many products
#[ORM\OneToMany(mappedBy: 'category', targetEntity: Product::class)]
private Collection $products;
Now you can go both ways: $product->getCategory() gives the category, and $category->getProducts() gives the collection of products.
Owning side versus inverse side
This is the part to understand. In the database, the relationship is one foreign key column, category_id on the products table. The side that holds that column is the owning side, and it is always the ManyToOne side. The other side is the inverse side, a convenience for reading.
The practical consequence: Doctrine only looks at the owning side when deciding what to save. So you set the relationship on the ManyToOne side:
$product->setCategory($category); // owning side, this is what persists
$em->flush();
Setting it only on the inverse side ($category->getProducts()->add($product)) without setting the owning side will not save the link. The make:entity command generates addProduct/removeProduct helpers on the inverse side that set both ends for you, which is why it is worth letting it scaffold the relationship.
ManyToMany
When both sides can have many of the other, an article has many tags and a tag belongs to many articles, that is a ManyToMany, and Doctrine manages the join table for you:
#[ORM\ManyToMany(targetEntity: Tag::class)]
private Collection $tags;
You work with it as a collection: $product->getTags()->add($tag). There is no separate entity for the join table unless you need extra columns on it, in which case you model it as two ManyToOne relationships through a middle entity instead.
Collections
The "many" side is always a Doctrine\Common\Collections\Collection, initialised as an ArrayCollection in the constructor:
public function __construct()
{
$this->products = new ArrayCollection();
}
You treat it like a list, iterating it in Twig or code, and it loads lazily, Doctrine only runs the query for the related rows when you actually access the collection.
Wrapping up
Doctrine models connections with relationship attributes: ManyToOne paired with OneToMany for the common "belongs to" case, and ManyToMany for the join-table case. The key rule is the owning side, the ManyToOne side that holds the foreign key, is the side Doctrine saves, so set the relationship there. Let make:entity scaffold the attributes and the collection helpers, initialise collections in the constructor, and you can navigate related entities as naturally as object properties. Once your schema has relationships, you keep it in sync with migrations.
All comments ()
No comments yet
Be the first to leave a comment on this post.