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.
What is StorageRepository? thanks