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
- 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
If not installed, add it (for example, with pecl install apcu) and enable it in your php.ini:
extension=apcu.so
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);
}
);
}
}
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'
];
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;
}
}
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)
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.
Thank you.
What is StorageRepository? thanks
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:
Structurally, it looks like this: