Бази даних та 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());
}
}
Спробуйте!
Вітаємо! Ви щойно створили ваш перший рядок у таблиці 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);
// ...
}
}
Спробуйте!
Коли ви запитуєте певний ти об'єкта, ви завжди використовуєте те, що відомо, як його "сховище". Ви можете думати про сховище як про 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-сторінки, панель інструментів веб-налагодження внизу сторінки відобразить кількість запитів та час, за який вони були виконані:
Якщо кількість запитів в базі даних занадто велика, іконка стане жовтою, щоб
показати, що щось може бути не так. Натисніть на іконку, щоб відкрити Профільувальник
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 для зміни об'єкта, необхідно зробити три кроки:
- отримати об'єкт з Doctrine;
- змінити об'єкт;
- викликати
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 для їхньої інтеграції у ваш застосунок.
Дізнайтеся більше
- Як працювати з асоціаціями / відносинами Doctrine
- Події Doctrine
- Яе реалізувати просту форму реєстрації
- Як зареєструвати користувацькі функції DQL
- Як використовувати DBAL Doctrine
- Як працювати з багатьма менеджерами сутностей та зʼєднаннями
- Як визначати відносини з абстрактними класами та інтерфейсами
- Як згенерувати сутності з існуючої бази даних
- Як тестувати сховище Doctrine