DEV Community

Test
Test

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
 
leobm_66 profile image
Felix Wittmann

What is StorageRepository? thanks