Doctrine ORM для PHP: полное руководство с примерами
1. Что это и зачем нужна?
Doctrine ORM (Object-Relational Mapper) — это мощная библиотека для PHP, реализующая паттерн Data Mapper. В отличие от Active Record (используемого в Laravel Eloquent), Doctrine полностью отделяет бизнес-логику (сущности) от логики работы с базой данных. Сущности — это обычные PHP-объекты (Plain Old PHP Objects, POPO), которые не знают о существовании базы данных. Вся работа с БД осуществляется через центральный репозиторий — EntityManager.
Зачем это нужно? Doctrine решает фундаментальную проблему несоответствия между объектно-ориентированной моделью данных и реляционной моделью базы данных (Object-Relational Impedance Mismatch). Вы работаете с объектами и коллекциями, а Doctrine автоматически транслирует ваши действия в SQL-запросы. Это позволяет писать более чистый, тестируемый и поддерживаемый код, особенно в крупных enterprise-проектах.
Ключевые преимущества: полный контроль над SQL (через DQL — Doctrine Query Language), ленивая загрузка (Lazy Loading), гибкая система событий (Lifecycle Events), поддержка наследования (Mapped Superclass, Single Table Inheritance, Joined Table Inheritance), встроенный Query Builder и гидраторы (Hydration Modes). Doctrine — стандарт де-факто для сложных PHP-приложений (Symfony, Drupal, Magento).
2. Установка
Установка производится через Composer (аналог pip для PHP). Для базового использования достаточно двух пакетов:
composer require doctrine/orm
composer require doctrine/dbal
composer require symfony/cache # Рекомендуется для кэширования метаданных
Для работы с аннотациями (устаревший, но популярный способ) требуется дополнительный пакет:
composer require doctrine/annotations
Для работы с YAML или XML конфигурацией:
composer require symfony/yaml # Для YAML
После установки убедитесь, что автозагрузчик Composer подключен в вашем проекте:
require_once __DIR__ . '/vendor/autoload.php';
3. Быстрый старт — минимальный рабочий пример
Создадим простую сущность User и сохраним её в базу данных SQLite.
Шаг 1: Конфигурация EntityManager (bootstrap.php)
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMSetup;
use Doctrine\DBAL\DriverManager;
require_once __DIR__ . '/vendor/autoload.php';
// Настройка путей к сущностям и режима разработки
$paths = [__DIR__ . '/src/Entity'];
$isDevMode = true;
// Создаем конфигурацию ORM
$config = ORMSetup::createAttributeMetadataConfiguration($paths, $isDevMode);
// Настройка подключения к БД (SQLite)
$connectionParams = [
'driver' => 'pdo_sqlite',
'path' => __DIR__ . '/data/database.sqlite',
];
$connection = DriverManager::getConnection($connectionParams, $config);
// Создаем EntityManager
$entityManager = new EntityManager($connection, $config);
Шаг 2: Создание сущности (src/Entity/User.php)
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'users')]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int|null $id = null;
#[ORM\Column(type: 'string', length: 100)]
private string $name;
#[ORM\Column(type: 'string', length: 255, unique: true)]
private string $email;
public function getId(): ?int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
}
Шаг 3: Создание схемы БД и сохранение данных (create_user.php)
require_once __DIR__ . '/bootstrap.php';
use App\Entity\User;
// Создание схемы таблиц
$schemaTool = new \Doctrine\ORM\Tools\SchemaTool($entityManager);
$classes = $entityManager->getMetadataFactory()->getAllMetadata();
$schemaTool->createSchema($classes);
// Создание нового пользователя
$user = new User();
$user->setName('Иван Петров');
$user->setEmail('ivan@example.com');
// Сохранение в БД
$entityManager->persist($user);
$entityManager->flush();
echo "Пользователь создан с ID: " . $user->getId() . PHP_EOL;
Запустите скрипт: php create_user.php. В папке data появится файл database.sqlite с таблицей users и одной записью.
4. Основные методы/функции/классы
4.1. EntityManager::persist()
Сигнатура: void persist(object $entity)
Описание: Сообщает EntityManager, что объект должен быть сохранен в БД при следующем вызове flush(). Объект переводится в состояние MANAGED.
$user = new User();
$user->setName('Анна');
$entityManager->persist($user);
// Объект еще не сохранен в БД
4.2. EntityManager::flush()
Сигнатура: void flush()
Описание: Выполняет все накопленные операции (INSERT, UPDATE, DELETE) в базе данных. Это ключевой метод, который синхронизирует состояние объектов с БД.
$entityManager->persist($user);
$entityManager->flush(); // Теперь данные записаны в БД
4.3. EntityManager::find()
Сигнатура: object|null find(string $className, mixed $id)
Описание: Находит сущность по её первичному ключу. Возвращает объект или null, если запись не найдена.
$user = $entityManager->find(User::class, 1);
if ($user) {
echo $user->getName(); // Иван Петров
}
4.4. EntityManager::getRepository()
Сигнатура: EntityRepository getRepository(string $entityName)
Описание: Возвращает репозиторий для работы с сущностями заданного класса. Репозиторий предоставляет методы для поиска (findBy, findOneBy, findAll) и кастомные запросы.
$userRepository = $entityManager->getRepository(User::class);
$users = $userRepository->findBy(['name' => 'Анна']); // Все Анны
$user = $userRepository->findOneBy(['email' => 'ivan@example.com']); // Один пользователь
4.5. QueryBuilder
Сигнатура: QueryBuilder createQueryBuilder()
Описание: Позволяет строить DQL-запросы программно, избегая написания сырого SQL. Обеспечивает безопасность от SQL-инъекций и удобство построения сложных запросов.
$queryBuilder = $entityManager->createQueryBuilder();
$query = $queryBuilder
->select('u')
->from(User::class, 'u')
->where('u.email LIKE :email')
->setParameter('email', '%@example.com')
->orderBy('u.name', 'ASC')
->setMaxResults(10)
->getQuery();
$users = $query->getResult();
4.6. EntityManager::remove()
Сигнатура: void remove(object $entity)
Описание: Помечает сущность для удаления. При следующем вызове flush() запись будет удалена из БД.
$user = $entityManager->find(User::class, 1);
$entityManager->remove($user);
$entityManager->flush(); // Пользователь удален
4.7. EntityManager::clear()
Сигнатура: void clear(string|null $entityName = null)
Описание: Очищает Unit of Work (кэш объектов). Все MANAGED объекты становятся DETACHED. Полезно для освобождения памяти при обработке больших объемов данных.
// Обработка 10000 записей
for ($i = 0; $i < 10000; $i++) {
$user = new User();
$user->setName("User $i");
$entityManager->persist($user);
if ($i % 500 === 0) {
$entityManager->flush();
$entityManager->clear(); // Очищаем память каждые 500 записей
}
}
$entityManager->flush();
4.8. Query::getResult() и Query::getSingleResult()
Сигнатура: array getResult(int $hydrationMode = Query::HYDRATE_OBJECT)
Описание: Выполняет запрос и возвращает результаты. getResult() возвращает массив, getSingleResult() — один объект (выбрасывает исключение, если результатов больше одного).
$query = $entityManager->createQuery('SELECT u FROM App\Entity\User u WHERE u.id > :id');
$query->setParameter('id', 5);
// Получить как объекты
$users = $query->getResult(); // Массив объектов User
// Получить как массивы (быстрее, меньше памяти)
$usersData = $query->getResult(\Doctrine\ORM\Query::HYDRATE_ARRAY);
// Получить один объект
$user = $query->setMaxResults(1)->getSingleResult();
4.9. EntityManager::createQuery()
Сигнатура: Query createQuery(string $dql = '')
Описание: Создает объект Query из строки DQL (Doctrine Query Language). DQL похож на SQL, но оперирует объектами и их свойствами.
$dql = "SELECT u FROM App\Entity\User u WHERE u.email = :email";
$query = $entityManager->createQuery($dql);
$query->setParameter('email', 'ivan@example.com');
$user = $query->getOneOrNullResult();
4.10. ClassMetadata и аннотации/атрибуты
Сигнатура: ClassMetadata getClassMetadata(string $className)
Описание: Предоставляет метаданные о сущности: имена полей, типы, связи, имя таблицы. Используется для динамического анализа структуры сущностей.
$metadata = $entityManager->getClassMetadata(User::class);
echo "Таблица: " . $metadata->getTableName(); // users
print_r($metadata->getFieldNames()); // ['id', 'name', 'email']
print_r($metadata->getAssociationNames()); // Связи (если есть)
5. Реальный практический пример (100-200 строк кода)
Создадим систему управления заказами с двумя сущностями: Customer и Order (связь OneToMany).
Сущность Customer (src/Entity/Customer.php):
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'customers')]
class Customer
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int|null $id = null;
#[ORM\Column(type: 'string', length: 150)]
private string $name;
#[ORM\Column(type: 'string', length: 255)]
private string $email;
#[ORM\OneToMany(mappedBy: 'customer', targetEntity: Order::class, cascade: ['persist', 'remove'])]
private Collection $orders;
public function __construct()
{
$this->orders = new ArrayCollection();
}
// Геттеры и сеттеры
public function getId(): ?int { return $this->id; }
public function getName(): string { return $this->name; }
public function setName(string $name): self { $this->name = $name; return $this; }
public function getEmail(): string { return $this->email; }
public function setEmail(string $email): self { $this->email = $email; return $this; }
public function getOrders(): Collection { return $this->orders; }
public function addOrder(Order $order): self
{
if (!$this->orders->contains($order)) {
$this->orders[] = $order;
$order->setCustomer($this);
}
return $this;
}
public function removeOrder(Order $order): self
{
if ($this->orders->removeElement($order)) {
if ($order->getCustomer() === $this) {
$order->setCustomer(null);
}
}
return $this;
}
}
Сущность Order (src/Entity/Order.php):
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'orders')]
class Order
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int|null $id = null;
#[ORM\Column(type: 'string', length: 50)]
private string $status = 'new';
#[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
private float $total = 0.0;
#[ORM\ManyToOne(targetEntity: Customer::class, inversedBy: 'orders')]
#[ORM\JoinColumn(nullable: false)]
private Customer|null $customer = null;
// Геттеры и сеттеры