Doctrine достаточно мощная система для работы с базами данных. Система для создания миграций и поддержания в актуальном состоянии структуру баз данных. Отличие Doctrine от других подобных систем в том, что Doctrine на основе созданных моделей и описанных в них полях определяет разницу структуры и создает миграции таким образом, чтобы после их запуска структура оказалась как описано в моделях.
Все необходимые конструкции находятся в пакете Doctrine\ORM\Mapping
который можно подклчюить как псевдоним, например Doctrine\ORM\Mapping as ORM
, описание будем производить через нотации
Объявление для Doctrine сущностей
-
в
config/packages/doctrine.yaml
в разделе mapping указан адрес, где Doctrine должна искать сущностиmappings: App: is_bundle: false type: annotation dir: '%kernel.project_dir%/src/Model/User/Entity' prefix: 'App\Model\User\Entity' alias: User
по этому адресу Dotrine будет сканировать и определять что является сущностью, что нет
-
Добавляем в нотации определение сущности и если нужно указать какую именно таблицу использовать, то указать соответствующие параметры:
/**
/* @ORM\Entity
* @ORM\Table(name="user_users", uniqueConstraints={
* @ORM\UniqueConstraint(columns={"email"}),
* @ORM\UniqueConstraint(columns={"reset_token_token"}),
* })
class User
{
}
Объявление полей
для каждого поля указываем тип через аннотации
/**
/* @var string
/* @ORM\Column(type="string")
*/
private $status;
варианты для полей
type="string"
length=16 # (default: 255)
nullable=true # (default: false)
type="datetime_immutable" # для даты/время
name="password_hash"
если названия полей в верблюжьей нотации (например: passwordHash), то для doctrine желательно переименовать, для этого есть параметр name="password_hash"
Вложенные объекты
/**
/* @ORM\Embeddable
*/
class Token
{
}
Аналогично необхходимо проставить типы для всех полей. Если вложенного объекта может не быть (например токен для запроса пароля появится только когда необходимо будет восстановить пароль), то необходимо проставить свойство "nullable=true"
Для связи вложенного объекта и сущности необходимо в сущности добавить в аннотациях информацию об этом. В сущности (например User) для соответствующего поля (например Token) в аннотациях необходимо добавить:
/**
/* @ORM\Embedded(class="Token", columnPrefix="token_")
*/
private $resetToken;
columnPrefix
нужен, чтобы Doctrine могла из таблицы забрать необходимые поля, которые будут названы по названия класса, в данном случае token_field_name
Проблемы
- при загрузке данных полей получается, что в поле, которое по сути является вложенным объектом, но для какой-то сущности должно быть пустым, то в случае с Dotrine окажется, что поле будет содержать объект, у которого будут пустые поля.
Для решения этого необходимо сбросить поле на null, т.е. запустить before/after… load.
Для этого необходимо указать для класса сущности (например User) нотацию@ORM\HasLifecycleCallbacks
и определить метод, который будет вызван и когда, например
/**
* @ORM\PostLoad()
*/
public function checkEmbeds(): void
{
if ($this->token->isEmpty()) {
$this->token = null;
}
}
Преобразование классов-сущностей-типов в поле в БД
Преобразование своих классов, которые являются некими типами, но которые необходимо преобразовать в поле в БД можно через создание своего типа Doctrine
.
Для этого:
- создаем свой тип, наследуясь от необходимо, рассмотрим например класс Email
<?php
namespace App\Model\User;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\StringType;
class EmailType extends StringType
{
public const NAME = 'user_user_email';
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
return $value instanceof Email ? $value->getValue() : $value;
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return !empty($value) ? new Email($value) : null;
}
public function getName()
{
return self::NAME;
}
}
-
добавим типы в файле конфигурации
config/packages/doctrine.yml
doctrine: dbal: ... types: user_user_email: 'App\Model\User\Entity\User\EmailType'
-
Используем тип в полях
/**
/* @var Email
/* @ORM\Column(type="user_user_email")
*/
private $email;
Ключевое поле (первичный ключ)
Чтобы указать, что поле является первичным ключом, необходимо добавить в нотацию @ORM\Id
а также если поле в сущности является объектом и преобразовывается в string, то добавить метод __toString()
:
public function __toString(): string
{
return $this->getValue();
}
Связи
- В соответствующей сущности (например Network) добавить уникальное поле, например
id
через аннотацию:/** * * @var string * @ORM\Column(type="guid") * @ORM\Id() */ private $id;
здесь поле id
является искусственным, поэтому можно использовать составные id, например user+network и для этого необходимо указывать нотацию @ORM\Id
для каждого из этих полей, либо пользоваться искусственным полем id
-
для связи необходимо использовать нотацию для указания типа связи и класс, с которым будет связь
/* @ORM\ManyToOne(targetEntity="User", inversedBy="networks")
-
указать колонку в бд, через которую будет производиться связь
/* @ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false, onDelete="CASCADE") */
-
в основной сущности, к которой идет привязка указанных сущностей, необходимо указать привязку
/* @ORM\OneToMany(targetEntity="Network", mappedBy="user", orphanRemoval=true, cascade={"persist"})
- с помощью
mappedBy="user"
вUser
иinversedBy="networks"
вNetwork
происходит заполнение с оответствующих полей необходимыми (соответствующими) объектами. cascade={"persist"})
— говорит чтобы при сохранении пользователя, так же сохранял и всеNetwork
которые присвоены кUser
orphanRemoval=true
— будет удалять устаревшие (освободившиеся) объекты
Миграции
Генерация миграций
php bin/console doctrine:migrations:diff
Применение миграций
php bin/console doctrine:migrations:migrate
Применение в ORM (Использование)
Можно создать репозиторий для получения данных. В будущем этот репозиторий можно изменить для использования других источников без изменения кода в целом.
Примерный код репозитория:
<?php
use Doctrine\ORM\EntityManagerInterface;
class UserRepository
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var \Doctrine\ORM\EntityRepository
*/
private $repo;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
$this->repo = $em->getRepository(User::class);
}
public function get(Id $id): User
{
/** @var User $user */
if (!$user = $this->repo->find($id->getValue())) {
throw new EntityNotFoundException('User is not found.');
}
return $user;
}
/**
* @param string $token
* @return User|object|null
*/
public function findByResetToken(string $token): ?User
{
return $this->repo->findOneBy(['resetToken.token' => $token]); ## здесь используется "промежуточный" язык Doctrine таким образом обращение к полям внутри findOneBy можно осуществлять через именования, которые использовались в классах (ResetToken->token) и т.п.
}
public function hasByNetworkIdentity(string $network, string $identity): bool
{
return $this->repo->createQueryBuilder('t')
->select('COUNT(t.id)')
->innerJoin('t.networks', 'n')
->andWhere('n.networks = :network and n.identity = :identity')
->setParameter(':network', $network)
->setParameter(':identity', $identity)
->getQuery()->getSingleScalarResult() > 0;
}
## Для связей таблиц или запроса из зависимых (связанных) таблиц можно использовать так же промежуточный синтаксис, т.е. достаточно указать какое поле будет участвовать в джойне, и, поскольку это поле уже описано как подмножество и определена и описана связь внутри моделей, то можно указать лишь поле в соотвествующей таблице и Doctrine уже сможет определить какую связь в этом случае реализовать и как описать.