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.

ORMPHPEntity MappingAssociationMetadata CacheDBAL

GitHub Overview

doctrine/orm

Doctrine Object Relational Mapper (ORM)

Stars10,074
Watchers247
Forks2,543
Created:April 6, 2010
Language:PHP
License:MIT License

Topics

hacktoberfest

Star History

doctrine/orm Star History
Data as of: 7/17/2025, 10:32 AM

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

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());