Skip to content

Doctrine ORM

Databases & CachingDatabase/ORMPHP

What it is

Doctrine is a Data Mapper ORM for PHP with a Unit of Work, its own query language (DQL), and a migrations system.

The EntityManager tracks loaded entities and computes the necessary SQL at flush time. DQL queries objects rather than tables.

Installation

composer require doctrine/orm doctrine/dbal

Getting started

The smallest useful thing you can do with it, and what each part means.

Unit of Work
<?php
$book = $em->find(Book::class, 42);

$book->setTitle('Dune Messiah');   // no query yet — just tracked
$book->getAuthor()->setName('F. Herbert');

$em->flush();   // one transaction, only the changed columns

// New entities must be told about explicitly.
$new = new Book('Children of Dune', 1976);
$em->persist($new);
$em->flush();
There is no save() per object. Doctrine diffs what changed and issues the minimal set of statements — but a new entity is invisible until persist() is called, which is the usual first surprise.
DQL and the query builder
<?php
// DQL queries entities and properties, not tables and columns.
$books = $em->createQuery(
    'SELECT b, a FROM App\Entity\Book b
     JOIN b.author a
     WHERE b.year >= :year
     ORDER BY b.year DESC'
)->setParameter('year', 1990)
 ->setMaxResults(10)
 ->getResult();

// Selecting `b, a` fetch-joins the author, avoiding N+1.

$qb = $repository->createQueryBuilder('b')
    ->where('b.year >= :year')->setParameter('year', 1990);

if ($search !== null) {
    $qb->andWhere('b.title LIKE :q')->setParameter('q', "%$search%");
}
Selecting both aliases in the SELECT clause is what actually hydrates the join — a JOIN without it still issues a second query per row when you touch the relation.

Advanced usage

Where the library earns its place over a simpler alternative.

Batch processing without exhausting memory
<?php
$batchSize = 100;
$query = $em->createQuery('SELECT b FROM App\Entity\Book b');

foreach ($query->toIterable() as $i => $book) {
    $book->recalculate();

    if (($i % $batchSize) === 0) {
        $em->flush();
        $em->clear();   // detach everything; otherwise memory grows unbounded
    }
}
$em->flush();
$em->clear();
The identity map keeps every loaded entity in memory. Without periodic clear(), a batch job over a large table will exhaust PHP's memory limit — this is Doctrine's most common production failure.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

A new entity was found through the relationship
A related object was never persisted. Add persist(), or configure cascade: ['persist'] on the association.
Allowed memory size exhausted in a batch job
The identity map is holding every entity. Use toIterable with periodic flush() and clear().

Best practices

  • Call persist() on new entities; only already-managed ones are tracked automatically.
  • Fetch-join relations by selecting both aliases in DQL to avoid N+1.
  • Clear the EntityManager periodically in batch jobs or memory grows without bound.
  • Use migrations rather than schema:update in production.

Background

Why it exists, and what it was reacting to.

Doctrine follows the Data Mapper pattern rather than Active Record: entities are plain objects that know nothing about persistence, which keeps domain logic independent of the database.