Skip to content

PHP Doctrine ORM — Complete Guide to Database Abstraction

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about PHP Doctrine ORM. We cover key concepts, practical examples, and best practices to help you master this topic.

Doctrine ORM is a powerful PHP ORM that provides transparent persistence for PHP objects using the Data Mapper pattern, DQL query language, and annotation or attribute mapping.

What You'll Learn

By the end of this tutorial, you'll configure Doctrine ORM, define entities, use the Entity Manager, write DQL queries, run migrations, and optimize performance.

Why Doctrine ORM Matters

Doctrine provides a complete ORM solution with mature tooling, DQL for database-agnostic queries, and strong support for complex domain models.

Real-World Use

An e-commerce application uses Doctrine ORM with entities for Product, Order, and Customer. The repository pattern encapsulates queries, and migrations manage schema changes across environments.

Doctrine ORM Path

flowchart LR
  A[PHP-DI Container] --> B[Doctrine ORM]
  B --> C[Entity Manager]
  B --> D[Repository]
  B --> E[Doctrine Relations]
  C --> F{You Are Here}
  B --> G[Doctrine Migrations]
  style F fill:#f90,color:#fff

Doctrine Setup

Configure Doctrine ORM with attribute mapping.

<?php
use Doctrine\DBAL\DriverManager;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMSetup;
require_once "vendor/autoload.php";
$paths = ["/path/to/entities"];
$isDevMode = true;
$config = ORMSetup::createAttributeMetadataConfiguration($paths, $isDevMode);
$connection = DriverManager::getConnection([
    "driver" => "pdo_mysql",
    "host" => "localhost",
    "dbname" => "myapp",
    "user" => "root",
    "password" => "",
], $config);
$entityManager = new EntityManager($connection, $config);

Defining Entities

Entities are PHP classes mapped with PHP attributes.

<?php
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: "products")]
class Product {
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: "integer")]
    private int|null $id = null;
    #[ORM\Column(type: "string", length: 255)]
    private string $name;
    #[ORM\Column(type: "decimal", precision: 10, scale: 2)]
    private float $price;
    #[ORM\Column(type: "datetime")]
    private DateTimeInterface $createdAt;
    public function __construct(string $name, float $price) {
        $this->name = $name;
        $this->price = $price;
        $this->createdAt = new DateTimeImmutable();
    }
    public function getId(): int|null { return $this->id; }
    public function getName(): string { return $this->name; }
    public function setName(string $name): void { $this->name = $name; }
}

Entity Manager Operations

Persist, flush, find, and remove entities.

<?php
$entityManager = EntityManagerFactory::create();
$product = new Product("Widget", 19.99);
$entityManager->persist($product);
$entityManager->flush();
$product = $entityManager->find(Product::class, 1);
$product->setName("Updated Widget");
$entityManager->flush();
$product = $entityManager->getRepository(Product::class)->findOneBy(["name" => "Widget"]);
$entityManager->remove($product);
$entityManager->flush();

Repository Pattern

Use repositories for custom queries.

<?php
use Doctrine\ORM\EntityRepository;
class ProductRepository extends EntityRepository {
    public function findCheaperThan(float $maxPrice): array {
        return $this->createQueryBuilder("p")
            ->where("p.price < :price")
            ->setParameter("price", $maxPrice)
            ->orderBy("p.price", "ASC")
            ->getQuery()
            ->getResult();
    }
    public function findByNameSearch(string $term): array {
        return $this->createQueryBuilder("p")
            ->where("p.name LIKE :term")
            ->setParameter("term", "%$term%")
            ->getQuery()
            ->getResult();
    }
}

Common Mistakes

1. N+1 Query Problem

Fetch entire collections without join. Use JOIN in DQL or eager loading to avoid.

2. Heavy Operations in the Unit of Work

Doctrine tracks all changes. For bulk operations, use DQL UPDATE/DELETE or batch processing.

3. Forgetting to Flush

Nothing persists until flush() is called. Always flush after changes.

4. Using Entities as DTOs

Entities are domain objects with behavior. Use DTOs for read-only queries.

5. Ignoring Second-Level Cache

Without Caching, every query hits the database. Enable second-level cache for read-heavy applications.

Practice Questions

1. What is the Data Mapper pattern?

Entities know nothing about the database. The ORM maps data between objects and tables.

2. How do you define a one-to-many relationship?

Use #[OneToMany] attribute on the collection property with mappedBy.

3. What is the Entity Manager?

Central access point for persistence operations. It manages the unit of work.

4. How does Doctrine handle transactions?

Entity Manager wraps operations in transactions automatically. Manual Transaction control is available.

5. Challenge: Create a Doctrine entity with lifecycle callbacks.

<?php
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
class BlogPost {
    #[ORM\PrePersist]
    public function onPrePersist(): void {
        $this->createdAt = new DateTimeImmutable();
    }
    #[ORM\PreUpdate]
    public function onPreUpdate(): void {
        $this->updatedAt = new DateTimeImmutable();
    }
}

FAQ

Is Doctrine ORM only for MySQL?

No. Doctrine supports MySQL, PostgreSQL, SQLite, SQL Server, and Oracle through DBAL.

What is DQL?

Doctrine Query Language - an SQL-like language that works with entity objects instead of tables.

What is the Unit of Work pattern?

Doctrine tracks changes to entities and flushes them in a single transaction.

How does Doctrine compare to Eloquent?

Doctrine uses Data Mapper. Eloquent uses Active Record. Doctrine is more flexible for complex domains.

Can I use raw SQL in Doctrine?

Yes. Use DBAL connection for native queries when needed.

Mini Project: Doctrine ORM CRUD

Build a complete CRUD application using Doctrine ORM.

<?php
use Doctrine\ORM\Tools\SchemaTool;
$config = DoctrineSetup::createConfiguration();
$entityManager = DoctrineSetup::createEntityManager($config);
$schemaTool = new SchemaTool($entityManager);
$classes = [$entityManager->getClassMetadata(Product::class)];
$schemaTool->createSchema($classes);
$product = new Product("Laptop", 999.99);
$entityManager->persist($product);
$entityManager->flush();
$products = $entityManager->getRepository(Product::class)->findAll();
foreach ($products as $p) {
    echo $p->getName() . " - $" . $p->getPrice() . "\n";
}

What's Next

Doctrine Relations Doctrine Migrations Doctrine DQL

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro