doctrine Создать свой тип данных для БД. Как сохранить в БД массив объектов - не энтити

Текст ошибки

The given entity of type ... has no identity/no id values set. It cannot be added to the identity map

Возможно, у Вас она связана с тем, что Вы пытаетесь положить в одну сущность другую, которая ещё не была сохранена, и соответственно ещё не получила id. Тогда либо сохраняйте объекты в БД поступательно, либо укажите cascade="persist" полю связи.

У меня же возникла при попытке положить в БД коллекцию объектов - не Entity, а ВО. Пишет, что id не найден (собственно, Value-Object и не имеет id, а только одно поле - $value).

Пыталась сохранить массив моих объектов в поле $merchantStatuses, Entity:

<?php

namespace App\Entity;

use App\VO\Status\MerchantStatus;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="agents_report")
 */
class AgentsReport
{
    /**
     * @var int
     *
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue
     */
    private $id;

    /**
     * @var Collection | MerchantStatus[]
     *
     * @ORM\Column(type="merchant_status")
     */
    private $merchantStatuses;
// ...
}

Ошибка в строке

$this->em->persist($agentsReport);

Решение

Я решила проблему с помощью создания Типа доктрины (Doctrine Types) для поля, содержащего коллекцию того, что мне нужно. (тип поля, который мы указываем доктрине в аннотации @ORM\Column(type="..")).

Что в этом Типе есть? В классе типа описывается, как обработать объект при записи в БД, и соответственно, как его обработать, когда из БД достаёшь, чтобы он выглядел "как новенький".

  1. Создаём класс. Вот так:
    <?php
    
    namespace App\Types;
    
    use App\VO\Status\MerchantStatus;
    use Doctrine\DBAL\Platforms\AbstractPlatform;
    use Doctrine\DBAL\Types\Type;
    use InvalidArgumentException;
    
    class TypeMerchantStatus extends Type
    {
        private const TYPE_NAME = 'merchant_status';
    
        /**
         * @param array            $fieldDeclaration
         * @param AbstractPlatform $platform
         *
         * @return string
         */
        public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
        {
            return $platform->getVarcharTypeDeclarationSQL($fieldDeclaration);
        }
    
        /**
         * @param string           $stringMrchantStatuses
         * @param AbstractPlatform $platform
         *
         * @return Collection | ReportOperationType[]
         *
         * @throws ConversionException
         */
        public function convertToPHPValue($stringMrchantStatuses, AbstractPlatform $platform): Collection
        {
            $decoded = json_decode($stringMrchantStatuses);
    
            if (JSON_ERROR_NONE !== json_last_error()) {
                throw ConversionException::conversionFailedSerialization(
                    $stringMrchantStatuses,
                    'json',
                    json_last_error_msg()
                );
            }
    
            $merchantStatuses = array_map(
                function (string $merchantStatus): MerchantStatus {
                    return new MerchantStatus($merchantStatus);
                },
                $decoded
            );
    
            return new ArrayCollection($merchantStatuses);
        }
        /**
         * @param ArrayCollection  $merchantStatuses
         * @param AbstractPlatform $platform
         *
         * @return string
         *
         * @throws InvalidArgumentException
         */
        public function convertToDatabaseValue($merchantStatuses, AbstractPlatform $platform): ?string
        {
            $arrayMerchantStatuses = $merchantStatuses->toArray();
    
            if (empty($arrayMerchantStatuses)) {
                return json_encode([]);
            }
    
            $encoded = json_encode($arrayMerchantStatuses);
    
            if (JSON_ERROR_NONE !== json_last_error()) {
                throw ConversionException::conversionFailedSerialization(
                    $merchantStatuses,
                    'json',
                    json_last_error_msg()
                );
            }
    
            return $encoded;
        }
    
        /**
         * @return string
         */
        public function getName(): string
        {
            return self::TYPE_NAME;
        }
    }

    Когда Doctrine видит тип поля merchant_statuses, она лезет в методы convertToPHPValue() и convertToDatabaseValue() и понимает, как именно конвертировать этого зверя в понятный ей формат и обратно. :)

  2. Не забываем зарегистиовать новый тип в конфиге doctrine.yaml:
    doctrine:
        dbal:
            types:
                merchant_status: App\Types\TypeMerchantStatus
    
  3. Ну и объект ВО надо модифицировать: добавляем ему реализацию интерфейса JsonSerializable (чтобы он мог прыгнуть в тип json) и метод jsonSerialize():
    <?php
    
    namespace App\VO\Status;
    
    use Doctrine\ORM\Mapping as ORM;
    use Doctrine\ORM\Mapping\Embeddable;
    use InvalidArgumentException;
    use JsonSerializable;
    
    /**
     * @Embeddable
     */
    class MerchantStatus implements JsonSerializable
    {
       // В этом объекте нет id, и это не Entity, но в БД нам надо положить коллекцию таких объектов
       // ...
        
        /**
         * @return string
         */
        public function jsonSerialize(): string
        {
            return $this->getValue();
        }
    }

Источники