Try Live
Add Docs
Rankings
Pricing
Docs
Install
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
Doctrine ORM
https://github.com/doctrine/orm
Admin
Doctrine ORM is an object-relational mapper for PHP that provides transparent persistence for PHP
...
Tokens:
185,900
Snippets:
1,524
Trust Score:
9.6
Update:
2 weeks ago
Context
Skills
Chat
Benchmark
94.3
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Doctrine ORM Doctrine ORM is an object-relational mapper for PHP 8.1+ that provides transparent persistence for PHP objects. It sits on top of a powerful database abstraction layer (DBAL) and allows developers to work with database records as PHP objects. One of its key features is the Doctrine Query Language (DQL), a proprietary object-oriented SQL dialect that provides powerful querying capabilities over your object model without requiring direct SQL manipulation. The ORM implements the Data Mapper pattern, enabling complete separation of domain/business logic from database persistence. Entities are PHP objects with identity that can be persisted and retrieved through the EntityManager, which serves as the primary interface for ORM operations. Doctrine uses metadata (via PHP attributes or XML) to describe how entities map to database tables, supporting complex relationships including one-to-one, one-to-many, many-to-one, and many-to-many associations. ## Installation and Configuration Install Doctrine ORM via Composer and configure the EntityManager with database connection settings and metadata paths. ```php <?php // composer.json { "require": { "doctrine/orm": "^3", "doctrine/dbal": "^4", "symfony/cache": "^7" } } // bootstrap.php require_once "vendor/autoload.php"; use Doctrine\DBAL\DriverManager; use Doctrine\ORM\EntityManager; use Doctrine\ORM\ORMSetup; // Create configuration for attribute-based metadata $config = ORMSetup::createAttributeMetadataConfig( paths: [__DIR__ . '/src'], isDevMode: true, ); // Database connection configuration $connection = DriverManager::getConnection([ 'driver' => 'pdo_mysql', 'user' => 'root', 'password' => '', 'dbname' => 'myapp', ], $config); // Obtain the EntityManager $entityManager = new EntityManager($connection, $config); ``` ## Entity Definition with Attributes Define entities using PHP 8 attributes to map classes to database tables with column types, primary keys, and generation strategies. ```php <?php // src/Product.php use Doctrine\ORM\Mapping as ORM; use Doctrine\DBAL\Types\Types; #[ORM\Entity] #[ORM\Table(name: 'products')] class Product { #[ORM\Id] #[ORM\Column(type: Types::INTEGER)] #[ORM\GeneratedValue] private int|null $id = null; #[ORM\Column(type: Types::STRING, length: 255)] private string $name; #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2)] private string $price; #[ORM\Column(type: Types::DATETIME_MUTABLE)] private \DateTime $createdAt; #[ORM\Column(type: Types::BOOLEAN, options: ['default' => true])] private bool $isActive = true; public function getId(): ?int { return $this->id; } public function getName(): string { return $this->name; } public function setName(string $name): void { $this->name = $name; } public function getPrice(): string { return $this->price; } public function setPrice(string $price): void { $this->price = $price; } public function getCreatedAt(): \DateTime { return $this->createdAt; } public function setCreatedAt(\DateTime $createdAt): void { $this->createdAt = $createdAt; } } ``` ## Persisting and Retrieving Entities Use the EntityManager to persist new entities, find existing ones, and flush changes to the database. ```php <?php // Creating and persisting a new entity $product = new Product(); $product->setName('Widget'); $product->setPrice('29.99'); $product->setCreatedAt(new DateTime()); $entityManager->persist($product); $entityManager->flush(); echo "Created Product with ID " . $product->getId() . "\n"; // Finding an entity by primary key $product = $entityManager->find(Product::class, 1); if ($product !== null) { echo "Found: " . $product->getName() . "\n"; } // Updating an entity (changes are tracked automatically) $product->setPrice('34.99'); $entityManager->flush(); // Only changed fields are updated // Removing an entity $entityManager->remove($product); $entityManager->flush(); ``` ## Entity Repository Methods Use repositories to query entities with built-in finder methods or custom repository classes. ```php <?php // Get the repository for an entity $productRepository = $entityManager->getRepository(Product::class); // Find all entities $allProducts = $productRepository->findAll(); // Find by primary key $product = $productRepository->find(1); // Find by criteria $activeProducts = $productRepository->findBy( ['isActive' => true], // criteria ['createdAt' => 'DESC'], // ordering 10, // limit 0 // offset ); // Find single entity by criteria $product = $productRepository->findOneBy(['name' => 'Widget']); // Count matching entities $count = $productRepository->count(['isActive' => true]); // Magic finder methods (via __call) $product = $productRepository->findOneByName('Widget'); $products = $productRepository->findByIsActive(true); ``` ## Many-To-One Association Map many-to-one relationships where multiple entities reference a single related entity. ```php <?php use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] class Comment { #[ORM\Id] #[ORM\Column(type: 'integer')] #[ORM\GeneratedValue] private int|null $id = null; #[ORM\Column(type: 'text')] private string $content; #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'comments')] #[ORM\JoinColumn(name: 'article_id', referencedColumnName: 'id')] private Article|null $article = null; public function getArticle(): ?Article { return $this->article; } public function setArticle(?Article $article): void { $this->article = $article; } } // Usage $article = $entityManager->find(Article::class, 1); $comment = new Comment(); $comment->setContent('Great article!'); $comment->setArticle($article); $entityManager->persist($comment); $entityManager->flush(); ``` ## One-To-Many Bidirectional Association Define bidirectional one-to-many relationships with collections on the inverse side. ```php <?php use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; #[ORM\Entity] class Article { #[ORM\Id] #[ORM\Column(type: 'integer')] #[ORM\GeneratedValue] private int|null $id = null; #[ORM\Column(type: 'string')] private string $title; /** @var Collection<int, Comment> */ #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'article', cascade: ['persist', 'remove'])] private Collection $comments; public function __construct() { $this->comments = new ArrayCollection(); } public function getComments(): Collection { return $this->comments; } public function addComment(Comment $comment): void { if (!$this->comments->contains($comment)) { $this->comments->add($comment); $comment->setArticle($this); } } public function removeComment(Comment $comment): void { if ($this->comments->removeElement($comment)) { $comment->setArticle(null); } } } // Usage $article = new Article(); $article->setTitle('Doctrine ORM Guide'); $comment = new Comment(); $comment->setContent('Very helpful!'); $article->addComment($comment); $entityManager->persist($article); $entityManager->flush(); ``` ## Many-To-Many Association Map many-to-many relationships using a join table with owning and inverse sides. ```php <?php use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; #[ORM\Entity] class User { #[ORM\Id] #[ORM\Column(type: 'integer')] #[ORM\GeneratedValue] private int|null $id = null; /** @var Collection<int, Role> */ #[ORM\ManyToMany(targetEntity: Role::class, inversedBy: 'users')] #[ORM\JoinTable(name: 'user_roles')] private Collection $roles; public function __construct() { $this->roles = new ArrayCollection(); } public function addRole(Role $role): void { if (!$this->roles->contains($role)) { $this->roles->add($role); $role->addUser($this); } } } #[ORM\Entity] class Role { #[ORM\Id] #[ORM\Column(type: 'integer')] #[ORM\GeneratedValue] private int|null $id = null; /** @var Collection<int, User> */ #[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'roles')] private Collection $users; public function __construct() { $this->users = new ArrayCollection(); } public function addUser(User $user): void { if (!$this->users->contains($user)) { $this->users->add($user); } } } ``` ## DQL SELECT Queries Use Doctrine Query Language (DQL) to query entities with an object-oriented SQL dialect. ```php <?php // Simple SELECT query $query = $entityManager->createQuery( 'SELECT u FROM App\Entity\User u WHERE u.age > :age' ); $query->setParameter('age', 18); $users = $query->getResult(); // JOIN with related entities (fetch join) $query = $entityManager->createQuery( 'SELECT a, c FROM App\Entity\Article a JOIN a.comments c WHERE a.isPublished = true ORDER BY a.createdAt DESC' ); $articles = $query->getResult(); // Aggregate functions $query = $entityManager->createQuery( 'SELECT COUNT(u.id) FROM App\Entity\User u WHERE u.isActive = true' ); $count = $query->getSingleScalarResult(); // Partial selection and grouping $query = $entityManager->createQuery( 'SELECT u.id, u.name, COUNT(o.id) AS orderCount FROM App\Entity\User u LEFT JOIN u.orders o GROUP BY u.id HAVING COUNT(o.id) > :minOrders' ); $query->setParameter('minOrders', 5); $results = $query->getResult(); // Pagination $query = $entityManager->createQuery('SELECT p FROM App\Entity\Product p'); $query->setFirstResult(0); $query->setMaxResults(10); $products = $query->getResult(); ``` ## DQL UPDATE and DELETE Queries Execute bulk update and delete operations directly in the database without loading entities. ```php <?php // Bulk UPDATE $query = $entityManager->createQuery( 'UPDATE App\Entity\User u SET u.status = :newStatus WHERE u.lastLogin < :date' ); $query->setParameter('newStatus', 'inactive'); $query->setParameter('date', new DateTime('-6 months')); $numUpdated = $query->execute(); echo "Updated $numUpdated users\n"; // Bulk DELETE $query = $entityManager->createQuery( 'DELETE FROM App\Entity\Session s WHERE s.expiresAt < :now' ); $query->setParameter('now', new DateTime()); $numDeleted = $query->execute(); echo "Deleted $numDeleted expired sessions\n"; // Clear identity map after bulk operations $entityManager->clear(); ``` ## QueryBuilder for Dynamic Queries Build DQL queries programmatically using the QueryBuilder fluent API. ```php <?php $qb = $entityManager->createQueryBuilder(); // Basic query $qb->select('u') ->from('App\Entity\User', 'u') ->where('u.isActive = :active') ->orderBy('u.name', 'ASC') ->setParameter('active', true); $users = $qb->getQuery()->getResult(); // Complex query with conditions $qb = $entityManager->createQueryBuilder(); $qb->select('p') ->from('App\Entity\Product', 'p') ->where($qb->expr()->andX( $qb->expr()->gte('p.price', ':minPrice'), $qb->expr()->lte('p.price', ':maxPrice'), $qb->expr()->orX( $qb->expr()->like('p.name', ':search'), $qb->expr()->like('p.description', ':search') ) )) ->setParameter('minPrice', 10) ->setParameter('maxPrice', 100) ->setParameter('search', '%widget%'); $products = $qb->getQuery()->getResult(); // Dynamic query building $qb = $entityManager->createQueryBuilder() ->select('o') ->from('App\Entity\Order', 'o'); if ($status !== null) { $qb->andWhere('o.status = :status') ->setParameter('status', $status); } if ($customerId !== null) { $qb->andWhere('o.customer = :customer') ->setParameter('customer', $customerId); } $orders = $qb->getQuery()->getResult(); ``` ## Query Result Hydration Modes Control how query results are returned using different hydration modes for performance optimization. ```php <?php use Doctrine\ORM\AbstractQuery; $dql = "SELECT u, a FROM App\Entity\User u JOIN u.address a"; $query = $entityManager->createQuery($dql); // Object hydration (default) - returns entity objects $users = $query->getResult(); // or explicitly: $users = $query->getResult(AbstractQuery::HYDRATE_OBJECT); // Array hydration - returns nested arrays (faster for read-only) $usersArray = $query->getArrayResult(); // Scalar hydration - flat result set with scalar values $scalars = $query->getScalarResult(); // Single scalar result (for COUNT, SUM, etc.) $query = $entityManager->createQuery('SELECT COUNT(u.id) FROM App\Entity\User u'); $count = $query->getSingleScalarResult(); // Single result (throws exception if not exactly one result) $query = $entityManager->createQuery('SELECT u FROM App\Entity\User u WHERE u.email = :email'); $query->setParameter('email', 'john@example.com'); $user = $query->getSingleResult(); // Single result or null $user = $query->getOneOrNullResult(); // Column result (single column as flat array) $query = $entityManager->createQuery('SELECT u.id FROM App\Entity\User u'); $ids = $query->getSingleColumnResult(); ``` ## Custom Repository Classes Create custom repository classes to encapsulate complex query logic and domain-specific finder methods. ```php <?php // src/Repository/UserRepository.php use Doctrine\ORM\EntityRepository; class UserRepository extends EntityRepository { public function findActiveUsers(): array { return $this->createQueryBuilder('u') ->where('u.isActive = true') ->orderBy('u.name', 'ASC') ->getQuery() ->getResult(); } public function findBySearchTerm(string $term, int $limit = 10): array { return $this->createQueryBuilder('u') ->where('u.name LIKE :term OR u.email LIKE :term') ->setParameter('term', '%' . $term . '%') ->setMaxResults($limit) ->getQuery() ->getResult(); } public function getStatistics(): array { return $this->createQueryBuilder('u') ->select('COUNT(u.id) as total, SUM(CASE WHEN u.isActive = true THEN 1 ELSE 0 END) as active') ->getQuery() ->getSingleResult(); } } // Entity configuration #[ORM\Entity(repositoryClass: UserRepository::class)] class User { // ... } // Usage $userRepo = $entityManager->getRepository(User::class); $activeUsers = $userRepo->findActiveUsers(); $searchResults = $userRepo->findBySearchTerm('john'); ``` ## Lifecycle Callbacks Define callback methods on entities that are triggered during persistence lifecycle events. ```php <?php use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Event\PrePersistEventArgs; use Doctrine\ORM\Event\PreUpdateEventArgs; #[ORM\Entity] #[ORM\HasLifecycleCallbacks] class Article { #[ORM\Column(type: 'datetime')] private \DateTime $createdAt; #[ORM\Column(type: 'datetime')] private \DateTime $updatedAt; #[ORM\Column(type: 'string')] private string $slug; #[ORM\PrePersist] public function onPrePersist(PrePersistEventArgs $args): void { $this->createdAt = new \DateTime(); $this->updatedAt = new \DateTime(); $this->generateSlug(); } #[ORM\PreUpdate] public function onPreUpdate(PreUpdateEventArgs $args): void { $this->updatedAt = new \DateTime(); if ($args->hasChangedField('title')) { $this->generateSlug(); } } private function generateSlug(): void { $this->slug = strtolower(preg_replace('/[^a-z0-9]+/i', '-', $this->title)); } } ``` ## Event Listeners and Subscribers Register external listeners for lifecycle events across multiple entity types. ```php <?php use Doctrine\ORM\Event\PreUpdateEventArgs; use Doctrine\ORM\Event\PostPersistEventArgs; use Doctrine\ORM\Events; use Doctrine\Common\EventSubscriber; // Event Listener class AuditListener { public function preUpdate(PreUpdateEventArgs $args): void { $entity = $args->getObject(); if ($entity instanceof AuditableInterface) { $entity->setModifiedAt(new \DateTime()); $entity->setModifiedBy($this->getCurrentUser()); } } public function postPersist(PostPersistEventArgs $args): void { $entity = $args->getObject(); $this->logger->info('Entity persisted: ' . get_class($entity)); } } // Event Subscriber class TimestampSubscriber implements EventSubscriber { public function getSubscribedEvents(): array { return [Events::prePersist, Events::preUpdate]; } public function prePersist($args): void { $this->updateTimestamps($args->getObject()); } public function preUpdate($args): void { $this->updateTimestamps($args->getObject()); } } // Registration $eventManager = $entityManager->getEventManager(); $eventManager->addEventListener([Events::preUpdate, Events::postPersist], new AuditListener()); $eventManager->addEventSubscriber(new TimestampSubscriber()); ``` ## Transactions and Explicit Control Manage database transactions explicitly for complex operations requiring atomicity. ```php <?php // Implicit transaction (automatic with flush) $user = new User(); $user->setName('John'); $entityManager->persist($user); $entityManager->flush(); // Transaction auto-committed // Explicit transaction control $entityManager->getConnection()->beginTransaction(); try { $user = new User(); $user->setName('John'); $entityManager->persist($user); $order = new Order(); $order->setUser($user); $entityManager->persist($order); $entityManager->flush(); $entityManager->getConnection()->commit(); } catch (\Exception $e) { $entityManager->getConnection()->rollBack(); throw $e; } // Using wrapInTransaction helper $entityManager->wrapInTransaction(function($em) { $user = new User(); $user->setName('Jane'); $em->persist($user); // flush is called automatically before commit }); ``` ## Optimistic Locking Implement optimistic locking to prevent concurrent modification conflicts in long-running transactions. ```php <?php use Doctrine\ORM\Mapping as ORM; use Doctrine\DBAL\LockMode; use Doctrine\ORM\OptimisticLockException; #[ORM\Entity] class Document { #[ORM\Id] #[ORM\Column(type: 'integer')] #[ORM\GeneratedValue] private int|null $id = null; #[ORM\Version] #[ORM\Column(type: 'integer')] private int $version; #[ORM\Column(type: 'text')] private string $content; } // Check version when loading $documentId = 1; $expectedVersion = 5; // Version from form hidden field try { $document = $entityManager->find( Document::class, $documentId, LockMode::OPTIMISTIC, $expectedVersion ); $document->setContent('Updated content'); $entityManager->flush(); } catch (OptimisticLockException $e) { echo "Document was modified by another user. Please reload and try again."; } // Or use lock() method $document = $entityManager->find(Document::class, $documentId); try { $entityManager->lock($document, LockMode::OPTIMISTIC, $expectedVersion); // Proceed with update } catch (OptimisticLockException $e) { // Handle conflict } ``` ## Native SQL Queries Execute raw SQL queries and map results to entities using ResultSetMapping. ```php <?php use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\ORM\Query\ResultSetMappingBuilder; // Using ResultSetMappingBuilder (recommended) $rsm = new ResultSetMappingBuilder($entityManager); $rsm->addRootEntityFromClassMetadata('App\Entity\User', 'u'); $sql = "SELECT u.id, u.name, u.email FROM users u WHERE u.status = ?"; $query = $entityManager->createNativeQuery($sql, $rsm); $query->setParameter(1, 'active'); $users = $query->getResult(); // With joined entity $rsm = new ResultSetMappingBuilder($entityManager); $rsm->addRootEntityFromClassMetadata('App\Entity\User', 'u'); $rsm->addJoinedEntityFromClassMetadata( 'App\Entity\Address', 'a', 'u', 'address', ['id' => 'address_id'] ); $sql = "SELECT u.id, u.name, a.id AS address_id, a.city FROM users u INNER JOIN addresses a ON u.address_id = a.id"; $query = $entityManager->createNativeQuery($sql, $rsm); $users = $query->getResult(); // Manual ResultSetMapping for complex queries $rsm = new ResultSetMapping(); $rsm->addEntityResult('App\Entity\User', 'u'); $rsm->addFieldResult('u', 'id', 'id'); $rsm->addFieldResult('u', 'name', 'name'); $rsm->addScalarResult('order_count', 'orderCount'); $sql = "SELECT u.id, u.name, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id"; $query = $entityManager->createNativeQuery($sql, $rsm); $results = $query->getResult(); ``` ## Schema Tool Commands Use command-line tools to manage database schema based on entity mappings. ```php <?php // bin/doctrine - CLI entry point use Doctrine\ORM\Tools\Console\ConsoleRunner; use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider; require __DIR__ . '/bootstrap.php'; ConsoleRunner::run(new SingleManagerProvider($entityManager)); // Available commands: // php bin/doctrine orm:schema-tool:create - Create database schema // php bin/doctrine orm:schema-tool:update - Update schema to match entities // php bin/doctrine orm:schema-tool:drop - Drop database schema // Example usage: // php bin/doctrine orm:schema-tool:update --force --dump-sql // Programmatic schema management use Doctrine\ORM\Tools\SchemaTool; $schemaTool = new SchemaTool($entityManager); $classes = $entityManager->getMetadataFactory()->getAllMetadata(); // Create schema $schemaTool->createSchema($classes); // Update schema $schemaTool->updateSchema($classes); // Get SQL without executing $sqls = $schemaTool->getUpdateSchemaSql($classes); foreach ($sqls as $sql) { echo $sql . "\n"; } ``` ## Inheritance Mapping Map class hierarchies to database tables using single table or class table inheritance strategies. ```php <?php use Doctrine\ORM\Mapping as ORM; // Single Table Inheritance - all classes in one table #[ORM\Entity] #[ORM\InheritanceType('SINGLE_TABLE')] #[ORM\DiscriminatorColumn(name: 'type', type: 'string')] #[ORM\DiscriminatorMap(['person' => Person::class, 'employee' => Employee::class])] class Person { #[ORM\Id] #[ORM\Column(type: 'integer')] #[ORM\GeneratedValue] protected int|null $id = null; #[ORM\Column(type: 'string')] protected string $name; } #[ORM\Entity] class Employee extends Person { #[ORM\Column(type: 'string')] private string $department; } // Class Table Inheritance - separate tables per class #[ORM\Entity] #[ORM\InheritanceType('JOINED')] #[ORM\DiscriminatorColumn(name: 'type', type: 'string')] #[ORM\DiscriminatorMap(['vehicle' => Vehicle::class, 'car' => Car::class, 'truck' => Truck::class])] class Vehicle { #[ORM\Id] #[ORM\Column(type: 'integer')] #[ORM\GeneratedValue] protected int|null $id = null; #[ORM\Column(type: 'string')] protected string $brand; } #[ORM\Entity] class Car extends Vehicle { #[ORM\Column(type: 'integer')] private int $seats; } // Query by type $query = $entityManager->createQuery('SELECT e FROM App\Entity\Employee e'); $employees = $query->getResult(); ``` ## Summary Doctrine ORM is the standard choice for database abstraction in PHP applications, particularly those built with Symfony. Its primary use cases include building data-driven web applications where domain objects need transparent database persistence, implementing complex domain models with rich relationships between entities, and developing applications requiring database portability across MySQL, PostgreSQL, SQLite, and other platforms. The ORM excels in scenarios requiring sophisticated querying through DQL, transactional integrity, and caching of entity metadata and query results. Integration patterns typically involve configuring the EntityManager as a shared service in dependency injection containers, defining entity mappings via PHP attributes for modern codebases or XML for configuration-driven approaches, and implementing the Repository pattern for encapsulating query logic. Applications commonly leverage lifecycle events for audit logging and automatic timestamp management, custom hydration modes for optimizing read-heavy operations, and the QueryBuilder for dynamic search functionality. For high-performance scenarios, developers combine DQL fetch joins to minimize N+1 queries, array hydration for read-only views, and native SQL for vendor-specific optimizations while maintaining the benefits of object mapping.