PHP Doctrine ORM — Complete Guide to Database Abstraction
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
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