DEV Community

Vladislav Malikov
Vladislav Malikov

Posted on • Edited on

Installing and Setting Up Doctrine ORM in Laravel 12

Integrating Doctrine ORM into your Laravel 12 project provides powerful object-relational mapping and flexible database management. By leveraging the APCu cache, you can dramatically boost performance for metadata and query caching. Here’s a step-by-step guide to a clean, maintainable, and high-performance setup.

1. Required Packages

First, install the essential packages via Composer:

composer require symfony/cache doctrine/orm doctrine/dbal 
Enter fullscreen mode Exit fullscreen mode
  • symfony/cache: Modern PSR-6/PSR-16 cache support;
  • doctrine/orm: Core ORM functionality;
  • doctrine/dbal: Database abstraction layer.

APCu PHP Extension:

Make sure the APCu extension is installed and enabled in your PHP environment. You can check this with:

php -m | grep apcu 
Enter fullscreen mode Exit fullscreen mode

If not installed, add it (for example, with pecl install apcu) and enable it in your php.ini:

extension=apcu.so 
Enter fullscreen mode Exit fullscreen mode

2. Why Use APCu?

Doctrine ORM benefits greatly from caching:

  • Metadata cache: Stores class mapping info, reducing parsing overhead;
  • Query cache: Caches DQL parsing for faster query execution.

APCu is an in-memory cache, making it extremely fast and ideal for single-server setups. It reduces database and CPU load, resulting in improved response times.

3. Example: Custom EntityManager Service Provider

Create a custom Laravel service provider to configure Doctrine ORM and APCu caching.

<?php declare(strict_types=1); namespace App\Shared\Infrastructure\Provider; use Illuminate\Support\ServiceProvider; use Illuminate\Foundation\Application; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManager; use Doctrine\ORM\ORMSetup; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Types\Type; final class DoctrineServiceProvider extends ServiceProvider { /** * Register any application services. */ public function register(): void { $this->app->singleton( abstract: EntityManagerInterface::class, concrete: function (Application $app): EntityManager { $config = ORMSetup::createAttributeMetadataConfiguration( paths: config( key: 'doctrine.metadata_dirs' ), isDevMode: config( key: 'doctrine.dev_mode' ), ); $connection = DriverManager::getConnection( params: config( key: 'doctrine.connection' ), config: $config ); foreach (config(key: 'doctrine.custom_types') as $name => $className) { if (!Type::hasType(name: $name)) { Type::addType( name: $name, className: $className ); } } return new EntityManager(conn: $connection, config: $config); } ); } } 
Enter fullscreen mode Exit fullscreen mode

Register this provider in your bootstrap/providers.php providers array.

4. Doctrine Configuration (config/doctrine.php)

Set up your connection and Doctrine settings:

<?php use App\Shared\Infrastructure\Id\RoleIdType; use App\Shared\Infrastructure\Id\PermissionIdType; use App\Shared\Infrastructure\Id\UserIdType; use App\Shared\Infrastructure\Id\MediaIdType; use App\Shared\Infrastructure\Slug\SlugType; return [ 'connection' => [ 'driver' => 'pdo_pgsql', 'host' => env('DB_HOST', 'postgres'), 'port' => env('DB_PORT', '5432'), 'dbname' => env('DB_DATABASE'), 'user' => env('DB_USERNAME'), 'password' => env('DB_PASSWORD'), 'options' => [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC ], ], 'metadata_dirs' => [ app_path(path: 'Account/Domain'), app_path(path: 'Privilege/Domain'), app_path(path: 'Media/Domain'), ], 'custom_types' => [ RoleIdType::NAME => RoleIdType::class, PermissionIdType::NAME => PermissionIdType::class, UserIdType::NAME => UserIdType::class, MediaIdType::NAME => MediaIdType::class, SlugType::NAME => SlugType::class, ], 'dev_mode' => env('APP_ENV') === 'dev' ]; 
Enter fullscreen mode Exit fullscreen mode

5. Summary Checklist

  • Install symfony/cache, doctrine/orm, and doctrine/dbal via Composer;
  • Enable the APCu PHP extension for high-speed in-memory caching;
  • Register a custom service provider to configure and instantiate Doctrine’s EntityManager;
  • Define your Doctrine configuration in config/doctrine.php, including connection details and metadata directories.

6. Performance Tips

  • APCu is best for single-server setups. For distributed systems, consider Redis or Memcached;
  • Use APCu for metadata and query cache. For result cache, you might want to use Redis for persistence;
  • Keep your APCu cache size and TTL (time-to-live) in check to avoid cache fragmentation.

7. Example of an entity

<?php declare(strict_types=1); namespace App\Account\Domain; use Doctrine\ORM\Mapping as ORM; use Doctrine\DBAL\Types\Types; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Auth\Authenticatable; use Tymon\JWTAuth\Contracts\JWTSubject; use Symfony\Component\Validator\Constraints as Assert; use Carbon\CarbonImmutable; use App\Shared\Domain\Date\CreatedDateProvider; use App\Shared\Domain\Date\UpdatedDateProvider; use App\Shared\Domain\Id\UserId; use App\Shared\Domain\Id\RoleId; use App\Shared\Domain\AggregateRoot; #[ORM\Entity] #[ORM\Table(name: 'users')] #[ORM\HasLifecycleCallbacks] final class User extends AggregateRoot implements JWTSubject, AuthenticatableContract { /** * Provides authentication methods for the user. */ use Authenticatable { Authenticatable::setRememberToken insteadof RememberToken; } /** * Automatically manages created_at and updated_at timestamps. */ use CreatedDateProvider; use UpdatedDateProvider; /** * Provides email verification functionality */ use EmailVerification; /** * Provides remember token management */ use RememberToken { RememberToken::setRememberToken as setCustomRememberToken; } /** * Unique identifier for the user. * * @var UserId */ #[ORM\Id] #[ORM\Column(name: 'id', type: UserId::class, unique: true)] public private(set) UserId $id; /** * The role's name. * * @var string */ #[Assert\NotBlank(message: 'Name should not be blank.')] #[Assert\Length( min: 2, max: 35, minMessage: 'Name must be at least {{ limit }} characters long.', maxMessage: 'Name cannot be longer than {{ limit }} characters.' )] #[ORM\Column(name: 'name', type: Types::STRING, length: 35)] public private(set) string $name { set (string $value) { $value = trim(string: $value); $value = mb_convert_case(string: $value, mode: MB_CASE_TITLE); $this->name = $value; } } /** * The user's email address. * * @var Email */ #[Assert\Valid] #[ORM\Embedded(class: Email::class, columnPrefix: false)] public private(set) Email $email; /** * The user's password. * * @var Password */ #[Assert\Valid] #[ORM\Embedded(class: Password::class, columnPrefix: false)] private Password $password; /** * The role of the user. * * @var RoleId|null */ #[Assert\Uuid(message: 'Role ID must be a valid UUID.', groups: ['Default'])] #[ORM\Column(name: 'role_id', type: RoleId::class, nullable: true)] public private(set) ?RoleId $roleId; /** * Initializes a new user with the given details. * * @param string $name * @param Email $email * @param Password $password * @param RoleId|null $roleId * @param UserId|null $id */ public function __construct( string $name, Email $email, Password $password, ?RoleId $roleId = null, ?UserId $id = null, ) { /** * Generates a new user ID if none is provided. */ $this->id = $id ?? UserId::generate(); /** * Assigns user personal and account details. */ $this->name = $name; $this->email = $email; $this->password = $password; /** * Sets the role identifier for the user. */ $this->roleId = $roleId; /** * Initialize created_at and updated_at timestamps. */ $this->initializeCreatedAt(); $this->initializeUpdatedAt(); } /** * Get the identifier that will be stored in the JWT subject claim. * * @return string */ public function getJWTIdentifier(): string { return $this->id->toString(); } /** * Get the custom claims to be added to the JWT. * * @return array<string, mixed> */ public function getJWTCustomClaims(): array { return [ 'id' => $this->id->asString(), 'email' => (string) $this->email, ]; } /** * Get the user's password object. * * @return Password */ public function getPassword(): Password { return $this->password; } /** * Set the user's password object. * * @param Password $password */ public function setPassword(Password $password): void { $this->password = $password; } } 
Enter fullscreen mode Exit fullscreen mode

Conclusion

This setup brings the power and flexibility of Doctrine ORM to your Laravel 12 application, with APCu providing a significant performance boost for metadata and query caching. The result is a clean, maintainable, and high-performing integration, ready for even demanding workloads.

Happy coding!

Top comments (4)

Collapse
 
xwero profile image
david duymelinck

A tip to make the code in the post easier to read. add php after the backticks that start the code.
This helps the syntax highlighter.

Collapse
 
initstack profile image
Vladislav Malikov

Thank you.

Collapse
 
leobm_66 profile image
Felix Wittmann

What is StorageRepository? thanks

Collapse
 
initstack profile image
Vladislav Malikov • Edited

StorageRepository is basically a repo for working with data from the database. I use this class inside the domain, separating read and write methods using the decorator pattern. Then, I save data from the cache into memory so I don’t have to hit the database every time, which speeds up data retrieval.

StorageRepository:

interface StorageRepositoryInterface extends RepositoryInterface { public function save(User $user): void; public function delete(User $user): void; } 
Enter fullscreen mode Exit fullscreen mode
abstract class StorageRepository extends Repository implements StorageRepositoryInterface { abstract protected function paginate(int $perPage = 11): LengthAwarePaginator; abstract protected function findById(UserId $id): ?User; } 
Enter fullscreen mode Exit fullscreen mode

Structurally, it looks like this:

Image description