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

Дата обновления перевода 2024-07-29

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

Screencast

Вы предпочитаете видео-уроки? Посмотрите Doctrine screencast series.

Symfony предоставляет все инструменты, которые вам нужны для использования баз данных в вашем приложении благодаря Doctrine, лучшему набору PHP библиотек для работы с базами данных. Эти инструменты поддерживают реляционные базы данных такие как MySQL и PostgreSQL, а также NoSQL базы данных такие как MongoDB

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

  • Эта статья объясняет рекомендованый способ работы с реляционными базами данных в приложениях Symfony;
  • Прочитайте о DBAL если вам нужен низкоуровневый доступ для выполнения напрямую 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(resolve: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;
    }

    // ... методы геттера и сеттера
}

Tip

Начиная с MakerBundle: v1.57.0 - Вы можете передавать --with-uuid или --with-ulid в make:entity. Используя Компонент Uid от Symfony, создается сущность с типом id в виде :ref:eb2558c24271a6187fd17b01b51f49fe9f4fe095int``.

Note

Начиная с v1.44.0 - MakerBundle поддерживает только сущности, использующие PHP-атрибуты.

Note

Удивлены почему цена это целое число? Не переживайте: это лишь пример. Но, если хранить цены как целые числа (например 100 = $1) можно избежать проблем с округлением.

Note

Если вы используете базу данных SQLite, вы увидите следующую ошибку: PDOException: SQLSTATE[HY000]: General error: 1 Невозможно добавить столбец NOT NULL cо значением по умолчанию 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 or USER). Смотрите документацю Doctrine Зарезериврованные ключевые слова SQL для деталей или как экранировать их. Или измените название таблицы @ORM\Table(name="groups") над классом или настройте название столбца в настройке name="group_name".

Миграции: Создание таблиц / схемы базы данных

Класс Product полностью сконфигурирован и готов к сохранению в таблицу product. Если вы только что создали класс, ваша база данных ещё не имеет таблицы product. Чтобы добавить её, можете использовать предустановленную DoctrineMigrationsBundle:

1
$ php bin/console make:migration

Tip

Начиная с MakerBundle: v1.56.0 - Передача --formatted в 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

Может ли это поле быть in в базе данных (nullable) (да/нет) [no]:
> no

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

Это также добавит новое свойство description и методы getDescription() и setDescription():

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Entity/Product.php
  // ...
+  use Doctrine\DBAL\Types\Types;

  class Product
  {
      // ...

+     #[ORM\Column(type: Types::TEXT)]
+     private string $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 doctrine:query:sql 'SELECT * FROM product'

# в системах Windows, не использующих Powershell, запустите эту команду:
# php bin/console doctrine:query:sql "SELECT * FROM product"

Рассмотрим предыдущий пример более детально:

  • строка 13 Аргумент EntityManagerInterface $entityManager указывает Symfony `внедрить сервис Entity Manager <services-constructor-injection>` в метод контроллера.
Этот объект отвечает за сохранение объектов в базу данных и
извлечение объектов их оттуда.
* строки 15-18 В этом разделе вы инстанцируете и работаете с объектом $product,
как с любым другим обычным объектом PHP.
* строка 21 Вызов persist($product) указывает Doctrine "управлять" объектом
объектом $product. Это не вызывает запрос к базе данных.
* строка 24 Когда вызывается метод flush(), Doctrine просматривает
все объекты, которыми она управляет, чтобы определить, нужно ли их сохранять в базу данных. В этом примере данные объекта $product не существуют в базе данных, поэтому менеджер сущностей выполняет запрос INSERT, создавая новую строку в таблице product.

Note

Если вызов flush() не успешный, то вызывается исключение Doctrine\ORM\ORMException. См. Транзакции и параллелизм.

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

Валидация объектов

Symfony validator повторно использует метаданные 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 Requires installing the PropertyInfo component
type Type Requires installing the PropertyInfo component
unique=true UniqueEntity  
length Length  

Так как компонент Form также как и API Platform внутри себя используют компонент Validator, все ваши формы и web API будут также автоматически получать пользу от этих автоматических ограничений валидации.

Автоматическая валидация - это удобно и увеличивает продуктивность, но она не заменяет полностью настройку валидации. Вам всё ещё нужно добавить несколько ограничений валидации (validation constraints), чтобы убедиться, что данные, предоставляемые пользователем, корректны.

Извлечение объектов из базы данных

Извлечение объекта обратно из 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 с autowiring Symfony и внедрить его через dependency injection container:

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

Когда вы запрашиваете определённый тип объекта, вы всегда используете то, что известно, как его "repository". Вы можете думать о repository, как о PHP-классе, единственной работой которого является помогать вам извлекать сущности определённого класса.

Когда у вас есть объект хранилища, у вас появляется множество helper-методов:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$repository = $doctrine->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.

Если количество запросов в DB слишком велико, иконка станет жёлтой, чтобы показать, что что-то может быть не так. Нажмите на иконку, чтобы открыть Symfony Profiler и посмотрите, какие именно запросы были выполнены. Если вы не видите панели инструментов веб-отладки, установите profiler Symfony pack запустив команду: 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. по колонке id. Если он не найден, то генерируется страница 404.

Tip

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

public function show(
#[CurrentUser] #[MapEntity(disabled: true)] User $user
): Response {
// Пользователь не разрешён 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 {
        // use the 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(), путём исключения одного или нескольких свойств, чтобы не использовались все:

#[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(objectManager: '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().

Запрашивание объектов: Хранилище

Вы уже видели, как объект repository позволяет вам выполнять базовые запросы без каких-либо усилий:

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 Query Language. Это позволяет вам создавать запросы используя популярный язык запросов, но ссылаться вместо таблиц на PHP-объекты (например в выражении FROM).

Теперь, вы можете вызать этот метод в repository:

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

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

// ...

См. , чтобы узнать как внедрить repository в любой сервис.

Выполнение запросов с конструктором запросов 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]);

        // возвращает массив массивов (т.e. сырой набор данных)
        return $stmt->fetchAllAssociative();
    }
}

С SQL, вы получите на выходе сырые данные, а не объекты (кроме случаев, когда вы используете функциональность NativeQuery).

Отношения и ассоциации

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

Чтобы узнать больше, см. Как работать с ассоциациями / отношениями Doctrine.

Расширения Doctrine (Timestampable, Translatable, и др.)

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