doctrine/orm
Doctrine ORM is a PHP 8.1+ object-relational mapper built on Doctrine DBAL, providing transparent persistence for PHP objects. Use mappings, repositories, and Unit of Work, plus DQL for powerful, object-oriented querying as an alternative to SQL.
Installation:
composer require doctrine/orm
Laravel already includes Doctrine DBAL (dependency of ORM), so no extra DBAL installation is needed.
Configuration:
Doctrine ORM requires a config.yml or equivalent. In Laravel, use the doctrine/orm package with a custom config file (e.g., config/doctrine.php):
return [
'default_connection' => 'default',
'connections' => [
'default' => [
'driver' => 'pdo_mysql',
'host' => env('DB_HOST'),
'port' => env('DB_PORT'),
'dbname' => env('DB_DATABASE'),
'user' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
'driverOptions' => [
PDO::MYSQL_ATTR_SSL_CA => env('DB_SSL_CA'),
],
],
],
'entity_managers' => [
'default' => [
'connection' => 'default',
'mappings' => [
'App' => [
'is_bundle' => false,
'type' => 'annotation',
'dir' => __DIR__.'/../app/Models',
'prefix' => 'App\Models',
'alias' => 'App',
],
],
],
],
];
First Use Case:
Define an entity (e.g., app/Models/User.php) with Doctrine annotations:
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: 'users')]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 180, unique: true)]
private string $email;
// Getters/setters...
}
Register the ORM in AppServiceProvider:
public function boot()
{
$config = config('doctrine');
$connection = Doctrine\DBAL\DriverManager::getConnection($config['connections']['default']);
$entityManager = Doctrine\ORM\EntityManager::create($connection, $config['entity_managers']['default']);
$this->app->singleton('doctrine.entity_manager', fn() => $entityManager);
}
Use the EntityManager in a service:
$user = $this->app['doctrine.entity_manager']->find(User::class, 1);
CRUD Operations:
// Create
$user = new User();
$user->setEmail('[email protected]');
$em->persist($user);
$em->flush();
// Read
$user = $em->find(User::class, 1);
$users = $em->getRepository(User::class)->findAll();
// Update
$user->setEmail('[email protected]');
$em->flush();
// Delete
$em->remove($user);
$em->flush();
Querying with DQL:
$query = $em->createQuery('SELECT u FROM App\Models\User u WHERE u.email LIKE :email')
->setParameter('email', '%test%');
$results = $query->getResult();
Repositories:
Extend Doctrine\ORM\EntityRepository for custom logic:
class UserRepository extends EntityRepository
{
public function findByEmail(string $email): ?User
{
return $this->findOneBy(['email' => $email]);
}
}
Transactions:
$em->beginTransaction();
try {
$em->persist($user);
$em->flush();
$em->commit();
} catch (\Exception $e) {
$em->rollback();
throw $e;
}
Relationships: Define associations in entities:
#[ORM\OneToMany(mappedBy: 'user', targetEntity: Post::class)]
private Collection $posts;
doctrine/dbal for schema migrations:
$schemaTool = new \Doctrine\ORM\Tools\SchemaTool($em);
$schemaTool->createSchema($em->getMetadataFactory()->getAllMetadata());
config/doctrine.php:
'entity_managers' => [
'default' => [
'query_cache_impl' => new \Doctrine\Common\Cache\ArrayCache(),
],
]
Lazy Loading:
Doctrine loads relationships lazily by default. Use fetch="EAGER" or join in queries to avoid N+1 queries:
#[ORM\ManyToOne(fetch: 'EAGER')]
private ?User $author;
Case Sensitivity: DQL is case-sensitive for identifiers. Use quotes for reserved keywords:
$query = $em->createQuery('SELECT u FROM App\Models\User u WHERE u.id = :id');
Connection Management: Ensure the connection is properly configured. Test with:
$connection = $em->getConnection();
$connection->getDatabasePlatform()->getSqlFormatter();
Circular References:
Avoid circular references in entity relationships (e.g., User ↔ Post ↔ User). Use inversedBy/mappedBy carefully.
Transaction Isolation: Long-running transactions can lock tables. Use shorter transactions or read-committed isolation:
$connection->beginTransaction(Connection::TRANSACTION_READ_COMMITTED);
SQL Logging:
Enable SQL logging in config/doctrine.php:
'entity_managers' => [
'default' => [
'logging' => true,
'connection' => [
'logging' => true,
'driver' => 'pdo_mysql',
'driverOptions' => [
PDO::MYSQL_ATTR_LOG_QUERY => true,
],
],
],
]
Check logs in storage/logs/laravel.log.
Query Profiling:
Use the Doctrine\ORM\Query\Query profiler:
$query->useResultCache(true);
$query->useQueryCache(true);
Hybrid Approach: Use Doctrine for complex queries and Eloquent for simple ones. Example:
// Doctrine DQL
$query = $em->createQuery('SELECT u FROM App\Models\User u WHERE u.createdAt > :date')
->setParameter('date', new \DateTime('-1 week'));
// Eloquent
$users = User::where('created_at', '>', now()->subWeek())->get();
Custom DQL Functions:
Register custom DQL functions in config/doctrine.php:
'entity_managers' => [
'default' => [
'dql' => [
'string_functions' => [
'CONCAT' => 'DoctrineExtensions\Query\Mysql\Concat',
],
],
],
]
Event Listeners: Use Doctrine events for pre/post operations:
$em->getEventManager()->addEventListener(
\Doctrine\ORM\Events::prePersist,
function ($event) {
$entity = $event->getEntity();
if ($entity instanceof User) {
$entity->setUpdatedAt(new \DateTime());
}
}
);
Second-Level Cache: Enable for read-heavy applications:
$em->getConfiguration()->setSecondLevelCacheEnabled(true);
$em->getConfiguration()->setSecondLevelCacheRegion('default');
Laravel Service Container: Bind the EntityManager as a singleton for dependency injection:
$this->app->bind('doctrine.entity_manager', fn() => $em);
Then inject it into controllers/services:
public function __construct(private EntityManager $em) {}
Schema Validation: Validate your schema before migrations:
$schemaTool = new \Doctrine\ORM\Tools\SchemaTool($em);
$schemaTool->updateSchema($em->getMetadataFactory()->getAllMetadata(), true);
How can I help you explore Laravel packages today?