Бази даних та Doctrine ORM

Дата оновлення перекладу 2024-06-07

Бази даних та Doctrine ORM

Screencast

Ви надаєте первагу відео-урокам? Подивіться Doctrine screencast series.

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

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

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

Установка Doctrine

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

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

Конфігурація бази даних

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# .env (або перевизначте DATABASE_URL в .env.local, щоб не додавати ваші зміни в репозиторій)

# налаштуйте цей рядок!
DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"

# для використання mariadb:
# До doctrine/dbal < 3.7
# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=mariadb-10.5.8"
# Починаючи з doctrine/dbal 3.7
# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=10.5.8-MariaDB"

# для використання sqlite:
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db"

# для використання postgresql:
# DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8"

# для використання oracle:
# DATABASE_URL="oci8://db_user:db_password@127.0.0.1:1521/db_name"

Caution

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

Тепер, коли ваші параметри з'єднання налаштовані, Doctrine може створити для вас DB db_name:

1
$ php bin/console doctrine:database:create

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

Tip

Існує багато інших команд Doctrine. Запустіть php bin/console list doctrine, щоб побачити повний перелік.

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ php bin/console make:entity

Ім'я класу сутності для створення або оновлення:
> Product

Нове ім'я властивості (натисніть <return>, щоб перестати додавати поля):
> name

Тип поля (введіть ?, щоб побчити всі типи) [string]:
> string

Довжина поля [255]:
> 255

Чи може це поле бути null в базі даних (nullable) (так/ні) [no]:
> no

Нове ім'я властивості (натисніть <return>, щоб перестати додавати поля):
> price

Тип поля (введите ?, чтобы увидеть все типы) [string]:
> integer

Чи може це поле бути null в базі даних (nullable) (так/ні) [no]:
> no

Новое имя свойства (натисніть <return>, щоб перестати додавати поля):
>
(натисніть enter знов, щоб закінчити)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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 and setter methods
}

Tip

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

Note

Починаючи з v1.44.0 - MakerBundle: підтримує тільки сутності, які використовують атрибути PHP.

Note

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

Note

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

Caution

Існує ліміт в 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".

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

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

Doctrine підтримує багато типів полів, кожний зі своїми налаштуваннями. Весь перелік можна подивитися в Документації типів відображення Doctrine. Якщо ви хочете використовувати XML замість анотацій, додайте type: xml та dir: '%kernel.project_dir%/config/doctrine' до відображення сутностей у вашому файлі config/packages/doctrine.yaml.

Caution

Будьте обережні та не використовуйте зарезервовані ключові слова SQL для назв таблиць або стовпчиків (наприклад, GROUP або USER). Дивіться документацію Doctrine Зарезервовані ключові слова SQL, щоб дізнаится деталі, так як екранувати їх. Або змініть назву таблиці @ORM\Table(name="groups") над класом або налаштуйте назву стовпчику в налаштуванні name="group_name".

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

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

1
$ php bin/console make:migration

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

1
2
3
4
УСПІХ!

Далі: Подивіться на нову міграцію "migrations/Version20211116204726.php"
Потім: Запустіть міграцію за допомогою php bin/console doctrine:migrations:migrate

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

1
$ php bin/console doctrine:migrations:migrate

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ php bin/console make:entity

Ім'я класу сутності для створення або оновлення
> Product

Нове ім'я властивості (натисніть <return>, щоб перестати додавати поля):
> description

Тип поля (введіть ?, щоб побачити всі типи) [string]:
> text

Чи може це поле бути в базі даних (nullable) (так/ні) [no]:
> no

Нове ім'я властивості (натисніть <return>, щоб перестати додавати поля):
>
(натисніть enter знов, щоб закінчити)

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

1
2
3
4
5
6
7
8
9
10
11
12
// src/Entity/Product.php
  // ...

  class Product
  {
      // ...

+     #[ORM\Column(type: 'text')]
+     private $description;

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

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

1
$ php bin/console make:migration

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

1
ALTER TABLE product ADD description LONGTEXT NOT NULL

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

1
$ php bin/console doctrine:migrations:migrate

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

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

Tip

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

1
$ php bin/console make:entity --regenerate

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

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

Прийшов час зберегти об'єкт Product в базу даних! Давайте створимо новий контролер для експериментів:

1
$ php bin/console make:controller ProductController

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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, що ви хочете (зрештою) зберенти Продукт (поки без запитів)
        $entityManager->persist($product);

        // дійсно виконайте запити (наприклад, запит INSERT)
        $entityManager->flush();

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

Спробуйте!

http://localhost:8000/product

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

1
2
3
4
$ 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 не існує в базі даних, тому менеджер сутностей виконує запит INSERT, створюючи новий рядок в таблиці product.

Note

Якщо виклик flush() не вдається, то викликається виключення Doctrine\ORM\ORMException. Див. Транзакції та паралелизм.

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

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

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

Розглянемо наступний код контролера:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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 будуть також автоматично отримувати користь від цих автоматичних обмежень валідації.

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

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

Отримання об'єкта з DB ще простіше. Уявіть, що ви хочете перейти в /product/1, щоб побачити ваш новий товар:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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 }}
        // поверне $this->render('product/show.html.twig', ['product' => $product]);
    }
}

Також можна використовувати ProductRepository з автомонтуванням Symfony та впровадити його через контейнер впровадження залежностей:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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-клас, єдиною роботою якого є допомагати вам вилучати сутності певного класу.

Коли у вас є об'єкт сховища, у вас з'являється безліч методів-хелперів:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$repository = $entityManager->getRepository(Product::class);

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

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

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

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

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

Tip

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

Панель інструментів веб-розробника з елементом Doctrine.

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

Для отримання додаткової інформації, прочитайте Документацію профілювальника Symfony.

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

2.7.1

Автомонтування EntityValueResolver було представлено в DoctrineBundle 2.7.1.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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
    {
        // викорирстати цей Продукт!
        // ...
    }
}

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

Tip

Якщо включено глобально, можливо відключити цю повіденку у конкретному контролері, використовуючи MapEntity, встановлений як disabled.

public function show(
#[CurrentUser] #[MapEntity(disabled: true)] User $user
): Response {
// User не розвʼязано EntityValueResolver // ...

}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * Отримати через основний ключ, так як {id} є у маршруті.
 */
#[Route('/product/{id}')]
public function showByPk(Product $product): Response
{
}

/**
 * Виконати findOneBy(), де властивість слага співпадає з {slug}.
 */
#[Route('/product/{slug}')]
public function showBySlug(Product $product): Response
{
}

Автоматичне отримання працює в таких ситуаціях:

  • Якщо {id} є у вашому маршруті, тоді він використовуєтьься для отримання основного ключа через метод find().
  • Розвʼязувач спробує зробити отримання findOneBy(), використовуючи всі підставні символи у вашому маршруті, які насправді є властивостями вашої сутності (не-властивості ігноруються).

Ця поведінка увімкнена за замовчуванням в усіх контролерах. За бажанням ви можете обмежити цю функцію лише роботою з підстановочними символами маршруту id, щоб шукати сутності за основним ключем. Для цього встановіть опцію doctrine.orm.controller_resolver.auto_mapping у значення false.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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:

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

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

Це також можна використати для розвʼязання багатьох аргументів:

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

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

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

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

Опції MapEntity

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

id

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

1
2
3
4
5
6
#[Route('/product/{product_id}')]
public function show(
    #[MapEntity(id: 'product_id')]
    Product $product
): Response {
}
mapping

Конфігурує властивості та значення для використання з методом findOneBy(): ключ - це имʼя заповнювачала маршруту, а значення - імʼя властивості Doctrine:

1
2
3
4
5
6
7
8
#[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 {
}
exclude

Конфігурує властивості, які маютьь бути використані у методі findOneBy() виключаючи одну або більше властивостей, щоб були використані не всі:

1
2
3
4
5
6
7
#[Route('/product/{slug}/{date}')]
public function show(
    #[MapEntity(exclude: ['date'])]
    Product $product,
    \DateTime $date
): Response {
}
stripNull
Якщо true, тоді за використання findOneBy(), будь-які значення null не будуть використані для запиту.
objectManager

За замовчуванням, EntityValueResolver використовує менеджер сутностей за замовчуванням, але ви можете це сконфігурувати:

1
2
3
4
5
6
#[Route('/product/{id}')]
public function show(
    #[MapEntity(entityManager: ['foo'])]
    Product $product
): Response {
}
evictCache
Якщо true, змушує Doctrine завжди отримувати сутність з бази даних замість кешу.
disabled
Якшо true, EntityValueResolver не буде намагатися замінити аргумент.
message

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

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

7.1

Опція message була представлена в Symfony 7.1.

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

Коли ви отримали об'єкт з Doctrine, можна взаємодіяти з ним так само, як і з будь-яким іншим PHP-об'єктом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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() в менеджері сутностей.

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

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

Видалення об'єкта дуже схоже, але потребує виклику методу remove() в менеджері сутностей:

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

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

Запит об'єктів: Сховище

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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 з ціною більшою, ніж задана. Додайте для цього у ваше сховище новий метод:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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);

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

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

Тепер ви можете викликати цей метод в сховищі:

1
2
3
4
5
6
// зсередини контролера
$minPrice = 1000;

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

// ...

Див. , щоб дізнатися як впровадити сховище в будь-який сервіс.

Виконання запитів з конструктором запитів Query Builder

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/Repository/ProductRepository.php

// ...
class ProductRepository extends ServiceEntityRepository
{
    public function findAllGreaterThanPrice(int $price, bool $includeUnavailableProducts = false): array
    {
        // автоматично знає, що треба обирати Продукти
        // "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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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 $stmt->fetchAllAssociative();
    }
}

З SQL, ви отримаєте на виході сирі дані, а не об'єкти (окрім випадків, коли ви використовуєте функціональність NativeQuery).

Відносини та асоціації

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

Щоб дізнатися більше, див. Як працювати з асоціаціями / відносинами Doctrine.

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

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