<? phpukraine ДОКУМЕНТАЦІЯ
Пошук по платформі
Документація українською

Переклад офіційної документації українською. Кожен розділ показує стан готовності: недоперекладене позначене відкрито, а не приховане.

ВЕРСІЯ
АКТУАЛЬНА Актуальний реліз. Переклад наздоганяє оригінал — розділи з низьким відсотком позначені у змісті.
DOCTRINE · ПЕРЕКЛАДЕНО оновлено 5 вересня 2026

Doctrine ORM — Symfony

Скрінкаст

Надаєте перевагу відеоурокам? Перегляньте серію скрінкастів про Doctrine.

Symfony надає всі інструменти, потрібні для роботи з базами даних у ваших застосунках, завдяки Doctrine — найкращому набору PHP-бібліотек для роботи з базами даних. Ці інструменти підтримують реляційні бази даних, як-от MySQL і PostgreSQL, а також NoSQL-бази даних, як-от MongoDB.

Бази даних — широка тема, тому документація поділена на три статті:

  • Ця стаття пояснює рекомендований спосіб роботи з реляційними базами даних у застосунках Symfony;
  • Прочитайте цю іншу статтю, якщо вам потрібен низькорівневий доступ для виконання сирих SQL-запитів до реляційних баз даних (подібно до PHP-розширення PDO);
  • Прочитайте документацію DoctrineMongoDBBundle, якщо ви працюєте з базами даних MongoDB.

Встановлення Doctrine

Спершу встановіть підтримку Doctrine через пакет Symfony orm, а також MakerBundle, який допоможе згенерувати частину коду:

$ composer require symfony/orm-pack
$ composer require --dev symfony/maker-bundle

Налаштування бази даних

Інформація про підключення до бази даних зберігається у змінній середовища DATABASE_URL. Для розробки ви можете знайти й змінити її у файлі .env:

# .env (або перевизначте DATABASE_URL у .env.local, щоб не комітити свої зміни)

# змініть цей рядок!
DATABASE_URL="mysql://db_user:[email protected]:3306/db_name?serverVersion=8.0.37"

# щоб використовувати mariadb:
# DATABASE_URL="mysql://db_user:[email protected]:3306/db_name?serverVersion=10.5.8-MariaDB"

# щоб використовувати локальний Unix-сокет замість TCP (це може трохи покращити продуктивність)
# (працює лише для баз даних MySQL/MariaDB, що запущені на Unix-подібних системах):
# DATABASE_URL="mysql://db_user:db_password@localhost/db_name?serverVersion=8.0.37&unix_socket=/var/run/mysqld/mysqld.sock"

# щоб використовувати sqlite:
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db"

# щоб використовувати postgresql:
# DATABASE_URL="postgresql://db_user:[email protected]:5432/db_name?serverVersion=12.19 (Debian 12.19-1.pgdg120+1)&charset=utf8"

# щоб використовувати oracle:
# DATABASE_URL="oci8://db_user:[email protected]:1521/db_name"

У цих прикладах використано 127.0.0.1 замість localhost, тому що localhost може резолвитися в IPv6-адресу (::1) і не з'єднатися, якщо сервер бази даних слухає лише IPv4. Використання 127.0.0.1 також поводиться однаково у Windows і в CI-середовищах.

Увага

Якщо імʼя користувача, пароль, хост або назва бази даних містять символи, які вважаються спеціальними в URI (як-от : / ? # [ ] @ ! $ & ' ( ) * + , ; =), їх треба закодувати. Повний список зарезервованих символів дивіться у RFC 3986. Для кодування можна скористатися функцією urlencode() або процесором змінних середовища urlencode. У цьому випадку потрібно прибрати префікс resolve: у config/packages/doctrine.yaml, щоб уникнути помилок: url: '%env(DATABASE_URL)%'

Щоб уникнути проблем з URL-кодуванням спеціальних символів в облікових даних, ви можете використати окремі параметри підключення замість формату URL. Визначте кожне значення як власну змінну середовища й візьміть її в одинарні лапки у файлі .env, щоб такі символи, як $ і #, не інтерпретувалися:

# .env
DATABASE_PASSWORD='p@ss$wo#rd'

Далі налаштуйте Doctrine на використання окремих параметрів:

# config/packages/doctrine.yaml
doctrine:
    dbal:
        user:     '%env(DATABASE_USER)%'
        password: '%env(DATABASE_PASSWORD)%'
        host:     '%env(DATABASE_HOST)%'
        port:     '%env(DATABASE_PORT)%'
        dbname:   '%env(DATABASE_NAME)%'
        driver:   pdo_mysql

Тепер, коли параметри підключення налаштовано, Doctrine може створити для вас базу даних db_name:

$ php bin/console doctrine:database:create

У config/packages/doctrine.yaml є ще більше опцій, які ви можете налаштувати, зокрема ваша server_version (наприклад, 8.0.37, якщо ви використовуєте MySQL 8.0.37), що може впливати на роботу Doctrine.

Є багато інших команд Doctrine. Виконайте php bin/console list doctrine, щоб побачити повний список.

Створення класу сутності

Припустімо, ви створюєте застосунок, у якому потрібно відображати товари. Навіть не думаючи про Doctrine чи бази даних, ви вже знаєте, що вам потрібен об'єкт Product, щоб представляти ці товари.

Ви можете скористатися командою make:entity, щоб створити цей клас і будь-які потрібні поля. Команда поставить вам кілька запитань — відповідайте так, як показано нижче:

$ php bin/console make:entity

Class name of the entity to create or update:
> Product

New property name (press <return> to stop adding fields):
> name

Field type (enter ? to see all types) [string]:
> string

Field length [255]:
> 255

Can this field be null in the database (nullable) (yes/no) [no]:
> no

New property name (press <return> to stop adding fields):
> price

Field type (enter ? to see all types) [string]:
> integer

Can this field be null in the database (nullable) (yes/no) [no]:
> no

New property name (press <return> to stop adding fields):
>
(press enter again to finish)

Ого! Тепер у вас є новий файл src/Entity/Product.php:

// src/Entity/Product.php
namespace App\Entity;

use App\Repository\ProductRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $name = null;

    #[ORM\Column]
    private ?int $price = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    // ... методи getter і setter
}

Ви можете передати команді make:entity опцію --with-uuid або --with-ulid. Використовуючи компонент Uid Symfony, це згенерує сутність, у якій тип id буде Uuid або Ulid замість int.

Примітка

Не розумієте, чому ціна — це ціле число? Не хвилюйтеся: це лише приклад. Але зберігання цін як цілих чисел (наприклад, 100 = $1 USD) дозволяє уникнути проблем із округленням.

Увага

Існує обмеження в 767 байтів на префікс ключа індексу при використанні таблиць InnoDB у MySQL 5.6 і раніших версіях. Рядкові стовпці довжиною 255 символів з кодуванням utf8mb4 перевищують це обмеження. Це означає, що будь-який стовпець типу string із unique=true має мати максимальну length рівну 190. Інакше ви побачите таку помилку: "[PDOException] SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes".

Цей клас називають «сутністю» (entity). І вже незабаром ви зможете зберігати об'єкти Product у таблицю product вашої бази даних і робити до неї запити. Кожну властивість сутності Product можна відобразити на стовпець у цій таблиці. Зазвичай це робиться за допомогою атрибутів #[ORM\Column(...)], які ви бачите над кожною властивістю:

Відображення Doctrine між властивостями PHP-об'єкта Product і даними в таблиці product бази даних

Команда make:entity — це інструмент, щоб полегшити життя. Але це ваш код: додавайте/видаляйте поля, додавайте/видаляйте методи або оновлюйте конфігурацію.

Увага

Будьте обережні й не використовуйте зарезервовані ключові слова SQL як назви таблиць чи стовпців (наприклад, GROUP або USER). Подробиці про те, як їх екранувати, дивіться в документації Doctrine про зарезервовані ключові слова SQL. Або змініть назву таблиці за допомогою #[ORM\Table(name: 'groups')] над класом чи налаштуйте назву стовпця опцією name: 'group_name'.

Типи полів сутності

Doctrine підтримує широку різноманітність типів полів (числа, рядки, переліки (enum), бінарні дані, дати, JSON тощо), кожен зі своїми опціями. Перегляньте список типів відображення Doctrine у документації Doctrine.

Один із цих типів базується на PHP-переліках (enumerations), які дозволяють визначити закритий набір можливих значень для певного типу. Це робить їх хорошим вибором для моделювання властивостей сутності, які мають приймати лише наперед визначений набір значень.

Перший крок — створити перелік (enum):

// src/Enum/Suit.php
namespace App\Enum;

enum Suit: string {
    case Hearts = 'H';
    case Diamonds = 'D';
    case Clubs = 'C';
    case Spades = 'S';
}

Примітка

Для властивостей сутності можна використовувати лише backed enum, оскільки Doctrine використовує їхні скалярні значення для збереження.

Коли перелік визначено, використайте опцію enumType атрибута #[ORM\Column], щоб пов'язати властивість із переліком:

// src/Entity/Card.php
namespace App\Entity;

#[Column(enumType: Suit::class)]
public Suit $suit;

Symfony також надає такі додаткові типи полів:

uuid

Клас: Symfony\Bridge\Doctrine\Types\UuidType

Зберігає UUID як нативний тип GUID, якщо він доступний, або як 16-байтовий бінарний тип в іншому разі:

// src/Entity/Product.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;

#[ORM\Entity]
class Product
{
    #[ORM\Column(type: UuidType::NAME)]
    private Uuid $sku;

    // ...
}

Докладніше, зокрема про використання UUID як первинних ключів, дивіться в розділі Зберігання UUID у базах даних документації компонента UID.

ulid

Клас: Symfony\Bridge\Doctrine\Types\UlidType

Зберігає ULID як нативний тип GUID, якщо він доступний, або як 16-байтовий бінарний тип в іншому разі:

// src/Entity/Product.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UlidType;
use Symfony\Component\Uid\Ulid;

#[ORM\Entity]
class Product
{
    #[ORM\Column(type: UlidType::NAME)]
    private Ulid $identifier;

    // ...
}

Докладніше, зокрема про використання ULID як первинних ключів, дивіться в розділі Зберігання ULID у базах даних документації компонента UID.

Типи DatePoint

Ці типи дозволяють зберігати об'єкти Symfony\Component\Clock\DatePoint із компонента Clock. Вони автоматично конвертуються в об'єкти DatePoint і назад.

Тип Розширює тип Doctrine Клас
date_point datetime_immutable Symfony\Bridge\Doctrine\Types\DatePointType
day_point date_immutable Symfony\Bridge\Doctrine\Types\DayPointType
time_point time_immutable Symfony\Bridge\Doctrine\Types\TimePointType

Приклад використання:

// src/Entity/Product.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Clock\DatePoint;

#[ORM\Entity]
class Product
{
    // Symfony автоматично визначає тип 'date_point' при вказанні типу DatePoint
    #[ORM\Column]
    private DatePoint $createdAt;

    // ви також можете задати тип явно
    #[ORM\Column(type: 'date_point')]
    private DatePoint $updatedAt;

    #[ORM\Column(type: 'day_point')]
    public DatePoint $releaseDate;

    #[ORM\Column(type: 'time_point')]
    public DatePoint $openingTime;

    // ...
}

Використовуйте date_point, коли хочете працювати з об'єктами Symfony\Component\Clock\DatePoint, — це полегшує тестування вашого коду за допомогою компонента Clock. Використовуйте datetime_immutable, якщо вам не потрібні можливості компонента Clock.

Міграції: створення таблиць/схеми бази даних

Клас Product повністю налаштований і готовий до збереження в таблицю product. Якщо ви щойно визначили цей клас, у вашій базі даних насправді ще немає таблиці product. Щоб її додати, ви можете скористатися DoctrineMigrationsBundle, який уже встановлено:

$ php bin/console make:migration

Передавання опції --formatted команді make:migration генерує гарний і охайний файл міграції.

Якщо все спрацювало, ви маєте побачити щось таке:

SUCCESS!

Next: Review the new migration "migrations/Version20211116204726.php"
Then: Run the migration with php bin/console doctrine:migrations:migrate

Якщо ви відкриєте цей файл, у ньому міститься SQL, потрібний для оновлення вашої бази даних! Щоб виконати цей SQL, запустіть свої міграції:

$ php bin/console doctrine:migrations:migrate

Ця команда виконує всі файли міграцій, які ще не були виконані для вашої бази даних. Ви маєте виконувати цю команду на продакшені під час деплою, щоб тримати продакшн-базу даних в актуальному стані.

Міграції та додавання нових полів

Але що, якщо вам треба додати до Product нову властивість-поле, наприклад description? Ви можете відредагувати клас і додати нову властивість. Але ви також можете знову скористатися make:entity:

$ php bin/console make:entity

Class name of the entity to create or update
> Product

New property name (press <return> to stop adding fields):
> description

Field type (enter ? to see all types) [string]:
> text

Can this field be null in the database (nullable) (yes/no) [no]:
> no

New property name (press <return> to stop adding fields):
>
(press enter again to finish)

Це додає нову властивість description та методи getDescription() і setDescription():

  // src/Entity/Product.php
  // ...
+ use Doctrine\DBAL\Types\Types;

  class Product
  {
      // ...

+     #[ORM\Column(type: Types::TEXT)]
+     private string $description;

      // getDescription() і setDescription() також були додані
  }

Нову властивість відображено, але в таблиці product вона ще не існує. Не проблема! Згенеруйте нову міграцію:

$ php bin/console make:migration

Цього разу SQL у згенерованому файлі виглядатиме так:

ALTER TABLE product ADD description LONGTEXT NOT NULL

Система міграцій розумна. Вона порівнює всі ваші сутності з поточним станом бази даних і генерує SQL, потрібний для їх синхронізації! Як і раніше, виконайте свої міграції:

$ php bin/console doctrine:migrations:migrate

Увага

Якщо ви використовуєте базу даних SQLite, ви побачите таку помилку: PDOException: SQLSTATE[HY000]: General error: 1 Cannot add a NOT NULL column with default value NULL. Додайте опцію nullable=true до властивості description, щоб виправити проблему.

Це виконає лише один новий файл міграції, тому що DoctrineMigrationsBundle знає, що першу міграцію вже було виконано раніше. Усередині він веде таблицю migration_versions, щоб це відстежувати.

Щоразу, коли ви змінюєте свою схему, виконуйте ці дві команди, щоб згенерувати міграцію, а потім виконати її. Не забувайте комітити файли міграцій і виконувати їх під час деплою.

Якщо ви віддаєте перевагу додаванню нових властивостей вручну, команда make:entity може згенерувати для вас методи getter і setter:

$ php bin/console make:entity --regenerate

Якщо ви внесли якісь зміни й хочете перегенерувати всі методи getter/setter, передайте також --overwrite.

Збереження об'єктів у базі даних

Настав час зберегти об'єкт Product у базі даних! Створімо новий контролер, щоб поекспериментувати:

$ php bin/console make:controller ProductController

Усередині контролера ви можете створити новий об'єкт Product, задати йому дані та зберегти його:

// src/Controller/ProductController.php
namespace App\Controller;

// ...
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class ProductController extends AbstractController
{
    #[Route('/product', name: 'create_product')]
    public function createProduct(EntityManagerInterface $entityManager): Response
    {
        $product = new Product();
        $product->setName('Keyboard');
        $product->setPrice(1999);
        $product->setDescription('Ergonomic and stylish!');

        // повідомляємо Doctrine, що ви хочете (згодом) зберегти Product (запитів поки що немає)
        $entityManager->persist($product);

        // власне виконує запити (тобто запит INSERT)
        $entityManager->flush();

        return new Response('Saved new product with id '.$product->getId());
    }
}

Спробуйте!

http://localhost:8000/product

Вітаємо! Ви щойно створили свій перший рядок у таблиці product. Щоб це підтвердити, ви можете зробити запит безпосередньо до бази даних:

$ php bin/console dbal:run-sql 'SELECT * FROM product'

# у системах Windows, де не використовується Powershell, виконайте натомість цю команду:
# php bin/console dbal:run-sql "SELECT * FROM product"

Розгляньмо попередній приклад детальніше:

  • рядок 13 Аргумент EntityManagerInterface $entityManager каже Symfony впровадити сервіс Entity Manager у метод контролера. Цей об'єкт відповідає за збереження об'єктів у базі даних і отримання об'єктів з неї.

  • рядки 15-18 У цій частині ви створюєте екземпляр об'єкта $product і працюєте з ним, як із будь-яким іншим звичайним PHP-об'єктом.

  • рядок 21 Виклик persist($product) каже Doctrine «керувати» об'єктом $product. Це не призводить до запиту до бази даних.

  • рядок 24 Коли викликається метод flush(), Doctrine переглядає всі об'єкти, якими він керує, щоб з'ясувати, чи потрібно зберегти їх у базу даних. У цьому прикладі даних об'єкта $product у базі даних немає, тож entity manager виконує запит INSERT, створюючи новий рядок у таблиці product.

Примітка

Якщо виклик flush() зазнає невдачі, буде викинуто виняток Doctrine\ORM\ORMException. Дивіться Transactions and Concurrency.

Незалежно від того, створюєте ви об'єкти чи оновлюєте, робочий процес завжди однаковий: Doctrine достатньо розумний, щоб знати, чи має він виконати INSERT, чи UPDATE для вашої сутності.

Валідація об'єктів

Валідатор Symfony може повторно використовувати метадані Doctrine, щоб виконати деякі базові завдання валідації. Спершу додайте або налаштуйте опцію auto_mapping, щоб визначити, які сутності Symfony має аналізувати для додавання автоматичних обмежень валідації.

Розгляньте такий код контролера:

// src/Controller/ProductController.php
namespace App\Controller;

use App\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
// ...

class ProductController extends AbstractController
{
    #[Route('/product', name: 'create_product')]
    public function createProduct(ValidatorInterface $validator): Response
    {
        $product = new Product();

        // ... якимось чином оновлюємо дані товару (наприклад, формою) ...

        $errors = $validator->validate($product);
        if (count($errors) > 0) {
            return new Response((string) $errors, 400);
        }

        // ...
    }
}

Хоча сутність Product не визначає жодної явної конфігурації валідації, якщо опція auto_mapping включає її до списку сутностей для аналізу, Symfony виведе для неї деякі правила валідації й застосує їх.

Наприклад, з огляду на те, що властивість name не може бути null у базі даних, до властивості автоматично додається обмеження NotNull (якщо воно ще не містить цього обмеження).

Наведена нижче таблиця підсумовує відповідність між метаданими Doctrine й обмеженнями валідації, які Symfony додає автоматично:

Атрибут Doctrine Обмеження валідації Примітки
nullable=false NotNull Потребує встановлення компонента PropertyInfo
type Type Потребує встановлення компонента PropertyInfo
unique=true UniqueEntity
length Length

Оскільки компонент Form, а також API Platform усередині використовують компонент Validator, усі ваші форми та веб-API також автоматично отримають переваги від цих автоматичних обмежень валідації.

Ця автоматична валідація — приємна можливість для підвищення продуктивності вашої роботи, але вона не замінює конфігурацію валідації повністю. Вам усе одно потрібно додавати деякі обмеження валідації, щоб гарантувати коректність даних, наданих користувачем.

Отримання об'єктів із бази даних

Отримати об'єкт назад із бази даних ще простіше. Припустімо, ви хочете мати змогу перейти на /product/1, щоб побачити свій новий товар:

// src/Controller/ProductController.php
namespace App\Controller;

use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
// ...

class ProductController extends AbstractController
{
    #[Route('/product/{id}', name: 'product_show')]
    public function show(EntityManagerInterface $entityManager, int $id): Response
    {
        $product = $entityManager->getRepository(Product::class)->find($id);

        if (!$product) {
            throw $this->createNotFoundException(
                'No product found for id '.$id
            );
        }

        return new Response('Check out this great product: '.$product->getName());

        // або відрендерити шаблон
        // у шаблоні виводьте значення через {{ product.name }}
        // return $this->render('product/show.html.twig', ['product' => $product]);
    }
}

Інша можливість — використати ProductRepository за допомогою автовайрингу Symfony, впроваджений контейнером впровадження залежностей:

// src/Controller/ProductController.php
namespace App\Controller;

use App\Entity\Product;
use App\Repository\ProductRepository;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
// ...

class ProductController extends AbstractController
{
    #[Route('/product/{id}', name: 'product_show')]
    public function show(ProductRepository $productRepository, int $id): Response
    {
        $product = $productRepository
            ->find($id);

        // ...
    }
}

Спробуйте!

http://localhost:8000/product/1

Коли ви робите запит на певний тип об'єкта, ви завжди використовуєте те, що відоме як його «репозиторій». Репозиторій можна уявляти як PHP-клас, єдина робота якого — допомогти вам отримувати сутності певного класу.

Щойно ви маєте об'єкт репозиторію, у вас з'являється багато допоміжних методів:

$repository = $entityManager->getRepository(Product::class);

// шукаємо один Product за його первинним ключем (зазвичай "id")
$product = $repository->find($id);

// шукаємо один Product за назвою
$product = $repository->findOneBy(['name' => 'Keyboard']);
// або шукаємо за назвою та ціною
$product = $repository->findOneBy([
    'name' => 'Keyboard',
    'price' => 1999,
]);

// шукаємо кілька об'єктів Product, що відповідають назві, впорядковані за ціною
$products = $repository->findBy(
    ['name' => 'Keyboard'],
    ['price' => 'ASC']
);

// шукаємо *всі* об'єкти Product
$products = $repository->findAll();

Ви також можете додавати власні методи для складніших запитів! Докладніше про це далі, у розділі Запити до об'єктів: репозиторій.

Під час рендерингу HTML-сторінки веб-панель налагодження внизу сторінки показуватиме кількість запитів і час, який знадобився на їх виконання:

Веб-панель розробника, що показує елемент Doctrine.

Якщо кількість запитів до бази даних завелика, іконка стане жовтою, вказуючи, що щось може бути не так. Клацніть на іконку, щоб відкрити Symfony Profiler і побачити точні запити, які було виконано. Якщо ви не бачите веб-панелі налагодження, встановіть пакет Symfony profiler, виконавши цю команду: composer require --dev symfony/profiler-pack.

Докладніше читайте в документації про Symfony profiler.

Автоматичне отримання об'єктів (EntityValueResolver)

У багатьох випадках ви можете використати EntityValueResolver, щоб він автоматично виконав запит за вас! Ви можете спростити контролер до:

// src/Controller/ProductController.php
namespace App\Controller;

use App\Entity\Product;
use App\Repository\ProductRepository;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
// ...

class ProductController extends AbstractController
{
    #[Route('/product/{id}')]
    public function show(Product $product): Response
    {
        // використовуйте Product!
        // ...
    }
}

Ось і все! Атрибут використовує {id} із маршруту, щоб зробити запит Product за стовпцем id. Якщо його не знайдено, викидається помилка 404.

Ви можете змінити цю поведінку, зробивши аргумент контролера необов'язковим. У такому разі 404 автоматично не викидається, і ви можете самі обробити відсутню сутність:

#[Route('/product/{id}')]
public function show(?Product $product): Response
{
    if (null === $product) {
        // виконайте власну логіку, щоб повернути власну відповідь
    }

    // ...
}

Коли поведінку ввімкнено глобально, її можна вимкнути для конкретного контролера, використавши MapEntity зі значенням disabled:

public function show(
    #[CurrentUser]
    #[MapEntity(disabled: true)]
    User $user
): Response {
    // User не резолвиться через EntityValueResolver
    // ...
}

Автоматичне отримання

За замовчуванням автоматичне отримання працює лише тоді, коли ваш маршрут містить підстановку {id}. Резолвер використовує її, щоб отримати сутність за її первинним ключем через метод find():

// виконує запит find($id), щоб знайти об'єкт $product
#[Route('/product/{id}')]
public function show(Product $product): Response
{
    // ...
}

Щоб отримувати сутності за іншими властивостями, використовуйте синтаксис маршруту {param:argument}. Він відображає параметр маршруту на аргумент контролера й каже резолверу робити запит до бази даних за цією властивістю:

// виконує запит findOneBy(['slug' => $slug]), щоб знайти об'єкт $product
#[Route('/product/{slug:product}')]
public function show(Product $product): Response
{
    // ...
}

Якщо ви встановите опцію doctrine.orm.controller_resolver.auto_mapping у true, резолвер спробує виконати findOneBy(), використовуючи всі підстановки маршруту, що відповідають властивостям вашої сутності (не-властивості ігноруються). Це усуває потребу в синтаксисі {param:argument}, але поведінка менш явна й більше не рекомендується.

Ви також можете явно налаштувати відображення для будь-якого аргумента контролера за допомогою атрибута MapEntity. Ви навіть можете керувати поведінкою EntityValueResolver за допомогою опцій MapEntity:

// src/Controller/ProductController.php
namespace App\Controller;

use App\Entity\Product;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
// ...

class ProductController extends AbstractController
{
    #[Route('/product/{slug}')]
    public function show(
        #[MapEntity(mapping: ['slug' => 'slug'])]
        Product $product
    ): Response {
        // використовуйте Product!
        // ...
    }
}

Отримання через вираз

Якщо автоматичне отримання не підходить для вашого випадку, ви можете написати вираз за допомогою компонента ExpressionLanguage:

#[Route('/product/{product_id}')]
public function show(
    #[MapEntity(expr: 'repository.find(product_id)')]
    Product $product
): Response {
}

У виразі змінна repository буде класом Repository вашої сутності, а будь-які підстановки маршруту — як-от {product_id} — доступні як змінні.

Метод репозиторію, викликаний у виразі, також може повертати список сутностей. У такому разі оновіть тип аргумента вашого контролера:

#[Route('/posts_by/{author_id}')]
public function authorPosts(
    #[MapEntity(class: Post::class, expr: 'repository.findBy({"author": author_id}, {}, 10)')]
    iterable $posts
): Response {
}

Це також можна використовувати, щоб допомогти резолвити кілька аргументів:

#[Route('/product/{id}/comments/{comment_id}')]
public function show(
    Product $product,
    #[MapEntity(expr: 'repository.find(comment_id)')]
    Comment $comment
): Response {
}

У прикладі вище аргумент $product обробляється автоматично, а $comment налаштовується атрибутом, оскільки обидва не можуть слідувати домовленості за замовчуванням.

Якщо вам потрібно отримати з запиту іншу інформацію, щоб зробити запит до бази даних, ви також можете звернутися до запиту у своєму виразі завдяки змінній request. Скажімо, ви хочете отримати перший або останній коментар до товару залежно від параметра запиту з назвою sort:

#[Route('/product/{id}/comments')]
public function show(
    Product $product,
    #[MapEntity(expr: 'repository.findOneBy({"product": id}, {"createdAt": request.query.get("sort", "DESC")})')]
    Comment $comment
): Response {
}

Отримання через інтерфейси

Припустімо, ваш клас Product реалізує інтерфейс під назвою ProductInterface. Якщо ви хочете відв'язати свої контролери від конкретної реалізації сутності, ви можете посилатися на сутність через її інтерфейс.

Щоб це увімкнути, спершу налаштуйте опцію resolve_target_entities. Далі ваш контролер може вказувати тип інтерфейсу, і сутність буде зарезолвлено автоматично:

public function show(
    #[MapEntity]
    ProductInterface $product
): Response {
    // ...
}

Опції MapEntity

Для атрибута MapEntity доступна низка опцій, що керують поведінкою:

id : Якщо налаштовано опцію id і вона відповідає параметру маршруту, резолвер шукатиме за первинним ключем:

#[Route('/product/{product_id}')]
public function show(
    #[MapEntity(id: 'product_id')]
    Product $product
): Response {
}

mapping : Налаштовує властивості й значення, які треба використати з методом findOneBy(): ключ — це назва підстановки маршруту, а значення — назва властивості Doctrine:

#[Route('/product/{category}/{slug}/comments/{comment_slug}')]
public function show(
    #[MapEntity(mapping: ['category' => 'category', 'slug' => 'slug'])]
    Product $product,
    #[MapEntity(mapping: ['comment_slug' => 'slug'])]
    Comment $comment
): Response {
}

stripNull : Якщо true, то під час використання findOneBy() будь-які значення, що є null, не використовуватимуться в запиті.

objectManager : За замовчуванням EntityValueResolver використовує типовий object manager, але ви можете це налаштувати:

#[Route('/product/{id}')]
public function show(
    #[MapEntity(objectManager: 'foo')]
    Product $product
): Response {
}

evictCache : Якщо true, змушує Doctrine завжди отримувати сутність із бази даних замість кешу.

disabled : Якщо true, EntityValueResolver не намагатиметься замінити аргумент.

message : Необов'язкове власне повідомлення, яке відображається, коли виникає Symfony\Component\HttpKernel\Exception\NotFoundHttpException, але лише в середовищі розробки (у продакшені ви цього повідомлення не побачите):

#[Route('/product/{product_id}')]
public function show(
    #[MapEntity(id: 'product_id', message: 'The product does not exist')]
    Product $product
): Response {
}

Оновлення об'єкта

Щойно ви отримали об'єкт із Doctrine, ви взаємодієте з ним так само, як із будь-якою PHP-моделлю:

// src/Controller/ProductController.php
namespace App\Controller;

use App\Entity\Product;
use App\Repository\ProductRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
// ...

class ProductController extends AbstractController
{
    #[Route('/product/edit/{id}', name: 'product_edit')]
    public function update(EntityManagerInterface $entityManager, int $id): Response
    {
        $product = $entityManager->getRepository(Product::class)->find($id);

        if (!$product) {
            throw $this->createNotFoundException(
                'No product found for id '.$id
            );
        }

        $product->setName('New product name!');
        $entityManager->flush();

        return $this->redirectToRoute('product_show', [
            'id' => $product->getId()
        ]);
    }
}

Використання Doctrine для редагування наявного товару складається з трьох кроків:

  1. отримання об'єкта з Doctrine;
  2. зміна об'єкта;
  3. виклик flush() на entity manager.

Ви можете викликати $entityManager->persist($product), але це не обов'язково: Doctrine уже «спостерігає» за вашим об'єктом на предмет змін.

Видалення об'єкта

Видалення об'єкта дуже схоже, але вимагає виклику методу remove() entity manager:

$entityManager->remove($product);
$entityManager->flush();

Як ви можете здогадатися, метод remove() повідомляє Doctrine, що ви хотіли б видалити заданий об'єкт із бази даних. Запит DELETE фактично не виконується, доки не буде викликано метод flush().

Запити до об'єктів: репозиторій

Ви вже бачили, як об'єкт репозиторію дозволяє виконувати базові запити без жодних зусиль:

// зсередини контролера
$repository = $entityManager->getRepository(Product::class);
$product = $repository->find($id);

Але що, як вам потрібен складніший запит? Коли ви генерували свою сутність за допомогою make:entity, команда також згенерувала клас ProductRepository:

// src/Repository/ProductRepository.php
namespace App\Repository;

use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

class ProductRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Product::class);
    }
}

Коли ви отримуєте свій репозиторій (тобто ->getRepository(Product::class)), це насправді екземпляр цього об'єкта! Так відбувається завдяки конфігурації repositoryClass, яку було згенеровано вгорі вашого класу сутності Product.

Припустімо, ви хочете зробити запит на всі об'єкти Product, дорожчі за певну ціну. Додайте до свого репозиторію новий метод для цього:

// src/Repository/ProductRepository.php

// ...
class ProductRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Product::class);
    }

    /**
     * @return Product[]
     */
    public function findAllGreaterThanPrice(int $price): array
    {
        $entityManager = $this->getEntityManager();

        $query = $entityManager->createQuery(
            'SELECT p
            FROM App\Entity\Product p
            WHERE p.price > :price
            ORDER BY p.price ASC'
        )->setParameter('price', $price);

        // повертає масив об'єктів Product
        return $query->getResult();
    }
}

Рядок, переданий у createQuery(), може виглядати як SQL, але це Doctrine Query Language. Це дозволяє писати запити добре відомою мовою запитів, але посилатися при цьому на PHP-об'єкти (тобто в інструкції FROM).

Тепер ви можете викликати цей метод на репозиторії:

// зсередини контролера
$minPrice = 1000;

$products = $entityManager->getRepository(Product::class)->findAllGreaterThanPrice($minPrice);

// ...

Про те, як впровадити репозиторій у будь-який сервіс, дивіться services-constructor-injection.

Запити за допомогою Query Builder

Doctrine також надає Query Builder — об'єктноорієнтований спосіб писати запити. Його рекомендується використовувати, коли запити будуються динамічно (тобто на основі PHP-умов):

// src/Repository/ProductRepository.php

// ...
class ProductRepository extends ServiceEntityRepository
{
    public function findAllGreaterThanPrice(int $price, bool $includeUnavailableProducts = false): array
    {
        // автоматично знає, що треба вибирати Product
        // "p" — це аліас, який ви використовуватимете в решті запиту
        $qb = $this->createQueryBuilder('p')
            ->where('p.price > :price')
            ->setParameter('price', $price)
            ->orderBy('p.price', 'ASC');

        if (!$includeUnavailableProducts) {
            $qb->andWhere('p.available = TRUE');
        }

        $query = $qb->getQuery();

        return $query->execute();

        // щоб отримати лише один результат:
        // $product = $query->setMaxResults(1)->getOneOrNullResult();
    }
}

Запити за допомогою SQL

Крім того, за потреби ви можете робити запити безпосередньо через SQL:

// src/Repository/ProductRepository.php

// ...
class ProductRepository extends ServiceEntityRepository
{
    public function findAllGreaterThanPrice(int $price): array
    {
        $conn = $this->getEntityManager()->getConnection();

        $sql = '
            SELECT * FROM product p
            WHERE p.price > :price
            ORDER BY p.price ASC
            ';

        $resultSet = $conn->executeQuery($sql, ['price' => $price]);

        // повертає масив масивів (тобто сирий набір даних)
        return $resultSet->fetchAllAssociative();
    }
}

За допомогою SQL ви отримаєте сирі дані, а не об'єкти (якщо тільки не використовуєте можливість NativeQuery).

Конфігурація

Дивіться довідник конфігурації Doctrine.

Звʼязки й асоціації

Doctrine надає всю функціональність, потрібну для керування звʼязками в базі даних (також відомими як асоціації), включно зі звʼязками ManyToOne, OneToMany, OneToOne і ManyToMany.

Докладніше дивіться в /doctrine/associations.

Тестування бази даних

Прочитайте статтю про тестування коду, що взаємодіє з базою даних.

Розширення Doctrine (Timestampable, Translatable тощо)

Спільнота Doctrine створила деякі розширення для реалізації поширених потреб, як-от «автоматично встановлювати значення властивості createdAt під час створення сутності». Прочитайте більше про доступні розширення Doctrine і використовуйте StofDoctrineExtensionsBundle, щоб інтегрувати їх у свій застосунок.

Дізнатися більше

ЯК ЦЯ СТОРІНКА ВИГЛЯДАЄ В ПОШУКУ
phpukraine.com/docs/symfony/doctrine
Doctrine ORM — Symfony документація українською
Doctrine ORM у Symfony 8.1: переклад офіційної документації українською. Оновлено 5 вересня 2026. Приклади коду, пояснення та посилання на питання зі співбесід.
Стан перекладу

Перекладаємо з офіційної документації, розділ за розділом, і не ховаємо недоперекладене. Помітили неточність у терміні чи реченні: напишіть, виправимо.

79%
Готовності
5
У роботі
0
Ще не перекладено
Глосарій термінів