Doctrine ORM
Doctrine ORM is a high-featured ORM library for PHP that provides "transparent persistence for PHP objects." It features a metadata system that maps entities to database tables, comprehensive association management including One-to-One, One-to-Many, and Many-to-Many relationships, and flexible mapping definitions using PHP 8+ attributes, XML, or PHP code. With high performance through metadata cache, query cache, and result cache, lazy loading via proxy objects, and advanced query processing through DQL, it strongly supports enterprise-level PHP application development.
GitHub Overview
doctrine/orm
Doctrine Object Relational Mapper (ORM)
Topics
Star History
Library
Doctrine ORM
Overview
Doctrine ORM is a high-featured ORM library for PHP that provides "transparent persistence for PHP objects." It features a metadata system that maps entities to database tables, comprehensive association management including One-to-One, One-to-Many, and Many-to-Many relationships, and flexible mapping definitions using PHP 8+ attributes, XML, or PHP code. With high performance through metadata cache, query cache, and result cache, lazy loading via proxy objects, and advanced query processing through DQL, it strongly supports enterprise-level PHP application development.
Details
Doctrine ORM 2025 edition leverages the latest PHP 8.1+ features, fully supporting modern PHP capabilities such as enumType, property hooks, and type inference. It implements diverse metadata definition methods through AttributeDriver, XMLDriver, and PHPDriver, efficient mapping information management via ClassMetadataFactory, and complete support for inheritance strategies (Single Table, Class Table, Mapped Superclass). Through extension points like PSR-6 compatible cache adapters, custom DQL functions, event listeners, and custom hydration modes, it enables advanced customization to handle complex business requirements.
Key Features
- Transparent Object Persistence: Automatic database mapping of PHP objects
- Comprehensive Associations: Complete support for One-to-One, One-to-Many, Many-to-Many
- Flexible Mapping: Diverse definition methods using PHP 8+ attributes, XML, PHP code
- Advanced Caching: Three-tier structure of metadata, query, and result caches
- Proxy and Lazy Loading: Efficient memory usage and performance optimization
- DQL Query Language: Object-oriented advanced query processing
Pros and Cons
Pros
- Rich functionality and stability as the most mature ORM in the PHP ecosystem
- Flexible mapping definition and metadata management via attributes, XML, PHP
- Comprehensive association management and complex relationship processing capabilities
- High performance through PSR-6 compliant three-tier cache system
- Excellent integration with major frameworks like Symfony and Laravel
- Extension features including inheritance mapping, custom types, event system
Cons
- Increased learning cost and memory usage due to large library size
- Initial setup difficulty due to configuration and metadata definition complexity
- Performance issues specific to ORM such as N+1 problems and lazy loading
- Runtime overhead from proxy generation and metadata processing
- Need for raw SQL or DBAL usage for complex queries
- Complexity of Data Mapper pattern compared to Active Record pattern
Reference Pages
- Doctrine ORM Official Documentation - Main Documentation
- Doctrine ORM GitHub - Source Code and Issues
- Doctrine DBAL Documentation - Database Abstraction Layer
- Symfony Doctrine Guide - Symfony Integration Guide
Code Examples
Basic Setup
# Install via Composer
composer require doctrine/orm
# Doctrine DBAL (Database Abstraction Layer)
composer require doctrine/dbal
# Development convenience tools
composer require --dev doctrine/doctrine-fixtures-bundle
composer require --dev doctrine/doctrine-migrations-bundle
<?php
// bootstrap.php - Doctrine configuration
use Doctrine\DBAL\DriverManager;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMSetup;
require_once "vendor/autoload.php";
// Entity metadata paths
$paths = [__DIR__."/src/Entity"];
$isDevMode = true;
// database configuration
$dbParams = [
'driver' => 'pdo_mysql',
'user' => 'root',
'password' => '',
'dbname' => 'myapp',
'host' => 'localhost',
'charset' => 'utf8mb4',
];
// ORM configuration
$config = ORMSetup::createAttributeMetadataConfiguration($paths, $isDevMode);
$connection = DriverManager::getConnection($dbParams, $config);
$entityManager = new EntityManager($connection, $config);
Model Definition and Basic Operations
<?php
// src/Entity/User.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
#[ORM\Entity]
#[ORM\Table(name: 'users')]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 255)]
private string $name;
#[ORM\Column(type: 'string', length: 255, unique: true)]
private string $email;
#[ORM\Column(type: 'datetime')]
private \DateTime $createdAt;
#[ORM\OneToMany(mappedBy: 'user', targetEntity: Post::class)]
private Collection $posts;
public function __construct()
{
$this->posts = new ArrayCollection();
$this->createdAt = new \DateTime();
}
// Getters and Setters
public function getId(): ?int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function getCreatedAt(): \DateTime
{
return $this->createdAt;
}
public function getPosts(): Collection
{
return $this->posts;
}
public function addPost(Post $post): self
{
if (!$this->posts->contains($post)) {
$this->posts[] = $post;
$post->setUser($this);
}
return $this;
}
}
<?php
// src/Entity/Post.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'posts')]
class Post
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 255)]
private string $title;
#[ORM\Column(type: 'text')]
private string $content;
#[ORM\Column(type: 'boolean')]
private bool $published = false;
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'posts')]
#[ORM\JoinColumn(nullable: false)]
private User $user;
#[ORM\Column(type: 'datetime')]
private \DateTime $createdAt;
public function __construct()
{
$this->createdAt = new \DateTime();
}
// Getters and Setters
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function getContent(): string
{
return $this->content;
}
public function setContent(string $content): self
{
$this->content = $content;
return $this;
}
public function isPublished(): bool
{
return $this->published;
}
public function setPublished(bool $published): self
{
$this->published = $published;
return $this;
}
public function getUser(): User
{
return $this->user;
}
public function setUser(User $user): self
{
$this->user = $user;
return $this;
}
public function getCreatedAt(): \DateTime
{
return $this->createdAt;
}
}
Advanced Query Operations
<?php
// src/Repository/UserRepository.php
namespace App\Repository;
use App\Entity\User;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
class UserRepository extends EntityRepository
{
public function findByEmailDomain(string $domain): array
{
return $this->createQueryBuilder('u')
->where('u.email LIKE :domain')
->setParameter('domain', '%@' . $domain)
->orderBy('u.createdAt', 'DESC')
->getQuery()
->getResult();
}
public function findUsersWithPosts(): array
{
return $this->createQueryBuilder('u')
->innerJoin('u.posts', 'p')
->addSelect('p')
->where('p.published = :published')
->setParameter('published', true)
->getQuery()
->getResult();
}
public function getUsersWithPostCount(): array
{
return $this->createQueryBuilder('u')
->select('u', 'COUNT(p.id) as postCount')
->leftJoin('u.posts', 'p')
->groupBy('u.id')
->having('COUNT(p.id) > 0')
->orderBy('postCount', 'DESC')
->getQuery()
->getResult();
}
public function findByCustomCriteria(array $criteria): QueryBuilder
{
$qb = $this->createQueryBuilder('u');
if (!empty($criteria['name'])) {
$qb->andWhere('u.name LIKE :name')
->setParameter('name', '%' . $criteria['name'] . '%');
}
if (!empty($criteria['email'])) {
$qb->andWhere('u.email = :email')
->setParameter('email', $criteria['email']);
}
if (!empty($criteria['dateFrom'])) {
$qb->andWhere('u.createdAt >= :dateFrom')
->setParameter('dateFrom', $criteria['dateFrom']);
}
return $qb;
}
}
Relation Operations
<?php
// Basic CRUD operations and relation management
require_once "bootstrap.php";
use App\Entity\User;
use App\Entity\Post;
// Get EntityManager
$entityManager = getEntityManager();
// CREATE - Creating user and posts
$user = new User();
$user->setName('John Doe');
$user->setEmail('[email protected]');
$post1 = new Post();
$post1->setTitle('First Post');
$post1->setContent('Learning how to use Doctrine ORM.');
$post1->setPublished(true);
$post1->setUser($user);
$post2 = new Post();
$post2->setTitle('Advanced Doctrine');
$post2->setContent('Implementing entity relationships.');
$post2->setPublished(false);
$post2->setUser($user);
// Persist
$entityManager->persist($user);
$entityManager->persist($post1);
$entityManager->persist($post2);
$entityManager->flush();
echo "User created with ID: " . $user->getId() . "\n";
// READ - Data retrieval
$userRepository = $entityManager->getRepository(User::class);
$postRepository = $entityManager->getRepository(Post::class);
// Retrieve by ID
$foundUser = $userRepository->find($user->getId());
echo "Found user: " . $foundUser->getName() . "\n";
// Search by criteria
$users = $userRepository->findBy(['email' => '[email protected]']);
foreach ($users as $u) {
echo "User: " . $u->getName() . " (" . $u->getEmail() . ")\n";
}
// Retrieve related data (lazy loading)
$userPosts = $foundUser->getPosts();
echo "User has " . $userPosts->count() . " posts\n";
foreach ($userPosts as $post) {
echo " - " . $post->getTitle() . " (Published: " . ($post->isPublished() ? 'Yes' : 'No') . ")\n";
}
// UPDATE - Data updates
$foundUser->setName('Jane Doe');
$post1->setPublished(false);
$entityManager->flush(); // Commit changes
// DELETE - Data deletion
$postToDelete = $postRepository->find($post2->getId());
if ($postToDelete) {
$entityManager->remove($postToDelete);
$entityManager->flush();
echo "Post deleted\n";
}
Practical Examples
<?php
// Advanced queries and transaction processing
// Using DQL queries
$dql = "SELECT u, p FROM App\Entity\User u
LEFT JOIN u.posts p
WHERE u.email LIKE :domain
AND p.published = :published
ORDER BY u.createdAt DESC";
$query = $entityManager->createQuery($dql);
$query->setParameter('domain', '%@example.com');
$query->setParameter('published', true);
$query->setMaxResults(10);
$results = $query->getResult();
foreach ($results as $user) {
echo "User: " . $user->getName() . "\n";
foreach ($user->getPosts() as $post) {
if ($post->isPublished()) {
echo " Published: " . $post->getTitle() . "\n";
}
}
}
// Native SQL queries
$sql = "SELECT u.name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE p.published = 1
GROUP BY u.id
ORDER BY post_count DESC";
$stmt = $entityManager->getConnection()->prepare($sql);
$result = $stmt->executeQuery();
$data = $result->fetchAllAssociative();
foreach ($data as $row) {
echo $row['name'] . ": " . $row['post_count'] . " posts\n";
}
// Transaction processing
$entityManager->beginTransaction();
try {
// Execute multiple operations within transaction
$newUser = new User();
$newUser->setName('Alice Smith');
$newUser->setEmail('[email protected]');
$newPost = new Post();
$newPost->setTitle('Transaction Test');
$newPost->setContent('Safely executing multiple operations.');
$newPost->setUser($newUser);
$newPost->setPublished(true);
$entityManager->persist($newUser);
$entityManager->persist($newPost);
// Commit only if all operations succeed
$entityManager->flush();
$entityManager->commit();
echo "Transaction completed successfully\n";
} catch (\Exception $e) {
// Rollback if error occurs
$entityManager->rollback();
echo "Transaction failed: " . $e->getMessage() . "\n";
}
// Batch processing (efficient handling of large data)
$batchSize = 100;
$counter = 0;
$query = $entityManager->createQuery('SELECT u FROM App\Entity\User u');
$iterableResult = $query->toIterable();
foreach ($iterableResult as $user) {
// Processing for each user
$user->setName($user->getName() . ' [Updated]');
if (++$counter % $batchSize === 0) {
// Flush and clear when batch size is reached
$entityManager->flush();
$entityManager->clear();
}
}
// Flush remaining changes
$entityManager->flush();
$entityManager->clear();
// Custom event listener example
use Doctrine\ORM\Events;
use Doctrine\Common\EventSubscriber;
use Doctrine\Persistence\Event\LifecycleEventArgs;
class PostSubscriber implements EventSubscriber
{
public function getSubscribedEvents(): array
{
return [
Events::prePersist,
Events::postPersist,
];
}
public function prePersist(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if ($entity instanceof Post) {
// Automatically generate slug when creating post
$slug = strtolower(str_replace(' ', '-', $entity->getTitle()));
// $entity->setSlug($slug);
echo "Creating post: " . $entity->getTitle() . "\n";
}
}
public function postPersist(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if ($entity instanceof Post) {
echo "Post created with ID: " . $entity->getId() . "\n";
}
}
}
// Register event listener
$eventManager = $entityManager->getEventManager();
$eventManager->addEventSubscriber(new PostSubscriber());