Doctrine Mapping в Symfony

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

Все необходимые конструкции находятся в пакете Doctrine\ORM\Mapping который можно подклчюить как псевдоним, например Doctrine\ORM\Mapping as ORM, описание будем производить через нотации

Объявление для Doctrine сущностей

  1. в 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 будет сканировать и определять что является сущностью, что нет

  2. Добавляем в нотации определение сущности и если нужно указать какую именно таблицу использовать, то указать соответствующие параметры:

/**
/* @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

Проблемы

  1. при загрузке данных полей получается, что в поле, которое по сути является вложенным объектом, но для какой-то сущности должно быть пустым, то в случае с Dotrine окажется, что поле будет содержать объект, у которого будут пустые поля.
    Для решения этого необходимо сбросить поле на null, т.е. запустить before/after… load.
    Для этого необходимо указать для класса сущности (например User) нотацию @ORM\HasLifecycleCallbacks и определить метод, который будет вызван и когда, например
/**
* @ORM\PostLoad()
*/
public function checkEmbeds(): void
{
    if ($this->token->isEmpty()) {
        $this->token = null;
    }
}

Преобразование классов-сущностей-типов в поле в БД

Преобразование своих классов, которые являются некими типами, но которые необходимо преобразовать в поле в БД можно через создание своего типа Doctrine.
Для этого:

  1. создаем свой тип, наследуясь от необходимо, рассмотрим например класс 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;
    }
}
  1. добавим типы в файле конфигурации config/packages/doctrine.yml

    doctrine:
    dbal:
        ...
        types:
            user_user_email: 'App\Model\User\Entity\User\EmailType'
  2. Используем тип в полях

/**
/* @var Email
/* @ORM\Column(type="user_user_email")
*/
private $email;

Ключевое поле (первичный ключ)

Чтобы указать, что поле является первичным ключом, необходимо добавить в нотацию @ORM\Id
а также если поле в сущности является объектом и преобразовывается в string, то добавить метод __toString():

public function __toString(): string
{
    return $this->getValue();
}

Связи

  1. В соответствующей сущности (например Network) добавить уникальное поле, например id через аннотацию:
    /**
    *
    * @var string
    * @ORM\Column(type="guid")
    * @ORM\Id()
    */
    private $id;

здесь поле id является искусственным, поэтому можно использовать составные id, например user+network и для этого необходимо указывать нотацию @ORM\Id для каждого из этих полей, либо пользоваться искусственным полем id

  1. для связи необходимо использовать нотацию для указания типа связи и класс, с которым будет связь

    /* @ORM\ManyToOne(targetEntity="User", inversedBy="networks") 
  2. указать колонку в бд, через которую будет производиться связь

    /* @ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false, onDelete="CASCADE") */
  3. в основной сущности, к которой идет привязка указанных сущностей, необходимо указать привязку

    /* @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 уже сможет определить какую связь в этом случае реализовать и как описать.

Leave a comment

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.