Для современных веб-приложений на PHP выбор ORM играет ключевую роль в организации работы с базой данных. В Laravel по умолчанию используется Eloquent — простой и удобный инструмент, идеально подходящий для большинства задач. Однако при сложных проектах, требующих гибкости, расширяемости и мощных возможностей работы с объектно-реляционным отображением — стоит обратить внимание на Doctrine ORM.

Во-первых, Doctrine обеспечивает более гибкое и мощное управление связями между сущностями, поддерживая различные типы ассоциаций и наследование. Во-вторых, она предлагает обширные возможности кэширования и оптимизации запросов, что улучшает производительность. Наконец, Doctrine использует шаблон Data Mapper, отделяя бизнес-логику от слоя данных, в отличие от Active Record в Eloquent, что способствует чистоте архитектуры и упрощает тестирование.

Установка необходимых пакетов

composer require symfony/cache doctrine/orm doctrine/dbal

Дополнительно можно установить пакет ramsey/uuid-doctrine для работы с UUID. В Laravel, например, уже есть удобный встроенный метод хелпера/фасада str()->uuid()->toString(), поэтому выбор зависит от ваших предпочтений и конкретных задач.

Еще важно убедиться, что у вас установлено и активировано расширение APCu. Для Doctrine ORM — APCu играет ключевую роль как механизм кэширования метаданных и результатов запросов. Это значительно повышает производительность приложения, так как сокращает количество обращений к базе данных и повторной обработки схемы. Без APCu Doctrine вынуждена каждый раз заново парсить аннотации и строить запросы, что замедляет работу.

Symfony Cache предоставляет общий интерфейс для кэширования, однако APCu обеспечивает быстрое хранение данных в памяти на уровне сервера, что критично для эффективной работы Doctrine и всего кэширования в проекте.

Настройка конфига для Doctrine ORM

В простейшем своем виде конфиг для Doctrine ORM в Laravel может выглядеть так:

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Database Connection
    |--------------------------------------------------------------------------
    |
    | Define your database connection parameters here. These values are pulled
    | from your environment file using the env() helper for security.
    | Customize driver, host, port, database name, username, password, and PDO options.
    | 
    | Available drivers supported by Doctrine ORM:
    | - pdo_mysql   (MySQL)
    | - pdo_pgsql   (PostgreSQL)
    | - pdo_sqlite  (SQLite)
    | - pdo_sqlsrv  (SQL Server / MSSQL)
    | - oci8        (Oracle)
    |
    | Make sure to set your DB_CONNECTION environment variable to one of these.
    |
    */
    'connection' => [
        'driver' => 'pdo_pgsql',
        'host' => env(key: 'DB_HOST', default: 'postgres'),
        'port' => env(key: 'DB_PORT', default: '5432'),
        'dbname' => env(key: 'DB_DATABASE'),
        'user' => env(key: 'DB_USERNAME'),
        'password' => env(key: 'DB_PASSWORD'),
        'options' => [
            \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
            \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Metadata Directories
    |--------------------------------------------------------------------------
    |
    | Paths where Doctrine will look for metadata with attributes to map your entities.
    | Typically, this points to your application's Entities directory.
    |
    */
    'metadata_dirs' => [
        app_path(path: 'Entities'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Custom Doctrine Types
    |--------------------------------------------------------------------------
    |
    | Register any custom Doctrine types here.
    | Example here registers UUID type from ramsey/uuid package.
    |
    */
    'custom_types' => [
        \Ramsey\Uuid\Doctrine\UuidType::NAME => \Ramsey\Uuid\Doctrine\UuidType::class,
    ],

    /*
    |--------------------------------------------------------------------------
    | Development Mode
    |--------------------------------------------------------------------------
    |
    | When set to true, Doctrine will generate proxies and metadata dynamically.
    | Typically enabled in local or dev environments.
    |
    */
    'dev_mode' => env(key: 'APP_ENV') === 'dev',
];

Вместо APCu можно в этом конфиге настроить Redis, вот пример: https://github.com/initialstacker/laravel-ddd/blob/main/src/config/doctrine.php, но опять же, это на вкус и цвет. Цель этой статьи — просто показать, как подружить Laravel 12 с Doctrine ORM.

Настройка сервис-провайдера

В Laravel для интеграции Doctrine ORM создайте сервис-провайдер, который зарегистрирует и сконфигурирует EntityManager как singleton в контейнере приложений.

Вот пример реализации класса DoctrineServiceProvider:

<?php declare(strict_types=1);

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Foundation\Application;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMSetup;
use Doctrine\ORM\Proxy\ProxyFactory;
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')
                );

                $config->setAutoGenerateProxyClasses(
                    autoGenerate: ProxyFactory::AUTOGENERATE_ALWAYS
                );

                $connection = DriverManager::getConnection(
                    params: config(key: 'doctrine.connection'),
                    config: $config
                );
                
                foreach (config(key: 'doctrine.custom_types') as $name => $type) {
                    if (!Type::hasType(name: $name)) {
                        Type::addType(name: $name, type: $type);
                    }
                }

                return new EntityManager(conn: $connection, config: $config);
            }
        );
    }
}

Этот провайдер позволяет удобно внедрять в приложение EntityManagerInterface и использовать Doctrine ORM с настраиваемыми параметрами, указанными в конфигурационных файлах Laravel.

Не забудьте только добавить его в bootstrap/providers.php.

Пример сущности Doctrine ORM

В качестве примера рассмотрим реализацию сущности User с использованием PHP 8.4 атрибутов (attributes) для описания маппинга с базой данных:

<?php declare(strict_types=1);

namespace App\Entities;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\DBAL\Types\Types;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\Doctrine\UuidGenerator;
use Ramsey\Uuid\UuidInterface;

#[ORM\Entity]
#[ORM\Table(name: '`users`')]
class User
{
    /**
     * Unique identifier for the user.
     *
     * @var UuidInterface
     */
    #[ORM\Id]
    #[ORM\Column(type: 'uuid', unique: true)]
    #[ORM\GeneratedValue(strategy: 'CUSTOM')]
    #[ORM\CustomIdGenerator(class: UuidGenerator::class)]
    public private(set) ?UuidInterface $id;

    /**
     * The user's name.
     *
     * @var string
     */
    #[ORM\Column(name: 'name', type: Types::STRING, length: 35)]
    public private(set) string $name {
        set (string $value) {
            $value = trim(string: $value);
            $name = mb_convert_case(string: $value, mode: MB_CASE_TITLE);

            $this->name = $name;
        }
    }

    /**
     * The user's email address.
     *
     * @var string
     */
    #[ORM\Column(name: 'email', type: Types::STRING, length: 254, unique: true)]
    public private(set) string $email;

    /**
     * The timestamp when the email was verified.
     *
     * @var \DateTimeImmutable|null
     */
    #[ORM\Column(
        name: 'email_verified_at',
        type: Types::DATETIME_IMMUTABLE,
        nullable: true
    )]
    private ?\DateTimeImmutable $emailVerifiedAt = null;

    /**
     * The user's password.
     *
     * @var Password
     */
    #[ORM\Column(name: 'password', type: Types::STRING, length: 60)]
    private string $password;

    /**
     * The token used for "remember me" functionality.
     *
     * @var string|null
     */
    #[ORM\Column(
        name: 'remember_token',
        type: Types::STRING,
        length: 100,
        unique: true,
        nullable: true
    )]
    private ?string $rememberToken = null {
        set (?string $value) {
            $rememberToken = $value !== null ? trim(string: $value) : null;
            $this->rememberToken = $rememberToken;
        }
    }

    /**
     * Timestamp when was created.
     *
     * @var \DateTimeImmutable
     */
    #[ORM\Column(name: 'created_at', type: Types::DATETIME_IMMUTABLE, options: [
        'precision' => 6,
        'default' => 'CURRENT_TIMESTAMP',
    ])]
    public private(set) ?\DateTimeImmutable $createdAt = null;

    /**
     * Timestamp when was last updated.
     *
     * @var \DateTimeImmutable
     */
    #[ORM\Column(name: 'updated_at', type: Types::DATETIME_IMMUTABLE, options: [
        'precision' => 6,
    ], nullable: true)]
    public private(set) ?\DateTimeImmutable $updatedAt = null;

    /**
     * Initializes a new user with the given details.
     *
     * @param string $name
     * @param string $email
     * @param string $password
     * @param UuidInterface|null $id
     */
    public function __construct(
        string $name,
        string $email,
        string $password,
        ?UuidInterface $id = null,
    ) {
        /**
         * Generates a new user ID if none is provided.
         */
        $this->id = $id ?? Uuid::uuid4();

        /**
         * Assigns user personal and account details.
         */
        $this->name = $name;
        $this->email = $email;
        $this->password = $password;

        /**
         * Initialize created_at and updated_at timestamps.
         */
        $this->createdAt = new \DateTimeImmutable();
        $this->updatedAt = new \DateTimeImmutable();
    }
}

В этом примере используется кастомный генератор UUID для первичного ключа через пакет ramsey/uuid-doctrine.

Пример запроса в Doctrine ORM

В этом примере показано, как с помощью Doctrine ORM и EntityManager получить всех пользователей из таблицы users через репозиторий сущности User:

<?php declare(strict_types=1);

namespace App\Http\Controllers;

use Doctrine\ORM\EntityManagerInterface;
use App\Entities\User;

final class UserController extends Controller
{
    /**
     * Doctrine EntityManager instance for DB operations.
     * 
     * @param EntityManagerInterface $entityManager
     */
    public function __construct(
        private EntityManagerInterface $entityManager
    ) {}

    /**
     * Handle the incoming request.
     */
    public function __invoke(): void
    {
        $users = $this->entityManager->getRepository(
            className: User::class
        )->findAll();

        dd($users);
    }
}

Объяснение:

  • EntityManagerInterface — это основной интерфейс для работы с Doctrine ORM, который позволяет взаимодействовать с базой данных.

  • Метод getRepository(ClassName::class) возвращает репозиторий для конкретной сущности, в данном случае User.

  • Вызов метода findAll() репозитория получает все записи из таблицы пользователей, возвращая массив объектов сущности.

Результат:

Заключение

Использование Doctrine ORM в Laravel предоставляет разработчикам возможность объединить мощь и гибкость Doctrine с удобством и функциональностью Laravel. Doctrine предлагает высокий уровень абстракции и более строгий паттерн Data Mapper, что позволяет отделить логику домена от способа хранения данных и использовать сложные типы данных, кэширование и расширенные возможности запросов через DQL. В то время как Laravel по умолчанию использует Eloquent с паттерном Active Record, интеграция Doctrine через пакеты, такие как Laravel Doctrine, позволяет получать преимущества обоих миров.

Doctrine ORM легко интегрируется с основными Laravel-компонентами — системой аутентификации, валидации и пагинации, что упрощает разработку и расширяет возможности приложения. Использование Doctrine с Laravel помогает организовать более чистую архитектуру, особенно в крупных проектах, где нужна сложная бизнес-логика и масштабируемость.

Комментарии (1)


  1. abyrvalg
    30.10.2025 05:29

    Используйте для идентификаторов UuidV7, субд вам спасибо скажет.