Custom DBAL Type Ignored During Hydration: TypeError When Using Collection in Entity #7453

Open
opened 2026-01-22 15:51:51 +01:00 by admin · 0 comments
Owner

Originally created by @pizgariu on GitHub (Dec 24, 2024).

Bug Report

Q A
Version 2.12.2
Previous Version if the bug is a regression Not applicable

Summary

When using a custom DBAL type that converts JSON data into a Doctrine\Common\Collections\Collection (e.g., ArrayCollection), Doctrine fails to properly handle hydration. Despite the type being registered and used in the entity, the convertToPHPValue method of the custom type is ignored during hydration, resulting in a TypeError. Doctrine tries to assign an array to a property typed as Collection, which leads to the error.

Current behavior

Doctrine does not use the custom DBAL type during hydration. Instead, it attempts to assign a raw array to the entity property, ignoring the expected conversion to Collection.

Expected behavior

Doctrine should respect the custom DBAL type and correctly convert the JSON data into the expected Collection instance during hydration.

How to reproduce

Provide a custom DBAL type:

namespace App\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use Doctrine\Common\Collections\ArrayCollection;

class JsonCollectionType extends Type
{
    const JSON_COLLECTION = 'json_collection';

    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getJsonTypeDeclarationSQL($column);
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        $data = json_decode($value, true);

        return new ArrayCollection($data ?? []);
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value instanceof ArrayCollection) {
            $value = $value->toArray();
        }

        return json_encode($value);
    }

    public function getName(): string
    {
        return self::JSON_COLLECTION;
    }
}

Register the type:

use Doctrine\DBAL\Types\Type;

if (!Type::hasType('json_collection')) {
    Type::addType('json_collection', \App\DBAL\Types\JsonCollectionType::class);
}

Define an entity:

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;

#[ORM\Entity]
class Product
{
    #[ORM\Column(type: "json_collection")]
    private Collection $paymentMethodsUuids;

    public function __construct()
    {
        $this->paymentMethodsUuids = new ArrayCollection();
    }

    public function getPaymentMethodsUuids(): Collection
    {
        return $this->paymentMethodsUuids;
    }

    public function setPaymentMethodsUuids(Collection $paymentMethodsUuids): void
    {
        $this->paymentMethodsUuids = $paymentMethodsUuids;
    }
}

When trying to hydrate this entity, you get the following error:

TypeError: Typed property App\Entity\Product::$paymentMethodsUuids must be an instance of Doctrine\Common\Collections\Collection, array used

Stacktrace

Click to expand stacktrace
{
    "errors": [
        {
            "class": "TypeError",
            "message": "Typed property Entity::$paymentMethodsUuids must be an instance of Collection, array used",
            "code": 0,
            "file": "/vendor/doctrine/persistence/src/Persistence/Reflection/TypedNoDefaultReflectionPropertyBase.php",
            "line": 64,
            "stacktrace": "#0 /vendor/doctrine/persistence/src/Persistence/Reflection/TypedNoDefaultReflectionPropertyBase.php(64): ReflectionProperty->setValue()\n#1 /vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php(2745): Persistence\\Reflection\\TypedNoDefaultReflectionProperty->setValue()\n#2 /vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php(255): ORM\\UnitOfWork->createEntity()\n#3 /vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php(481): ORM\\Internal\\Hydration\\ObjectHydrator->getEntity()\n#4 /vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php(143): ORM\\Internal\\Hydration\\ObjectHydrator->hydrateRowData()\n#5 /vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php(270): ORM\\Internal\\Hydration\\ObjectHydrator->hydrateAllData()\n#6 /vendor/doctrine/orm/lib/Doctrine/ORM/AbstractQuery.php(1201): ORM\\Internal\\Hydration\\AbstractHydrator->hydrateAll()\n#7 /vendor/doctrine/orm/lib/Doctrine/ORM/AbstractQuery.php(1142): ORM\\AbstractQuery->executeIgnoreQueryCache()\n#8 /vendor/doctrine/orm/lib/Doctrine/ORM/AbstractQuery.php(878): ORM\\AbstractQuery->execute()\n#9 /src/Repository/ProductRepository.php(137): ORM\\AbstractQuery->getResult()\n#10 /src/Handler/Query/GetActiveProductQueryHandler.php(18): Repository\\ProductRepository->findActivePlans()\n#11 /vendor/symfony/messenger/Middleware/HandleMessageMiddleware.php(95): Handler\\Query\\GetActiveProductQueryHandler->__invoke()\n#12 /vendor/symfony/messenger/Middleware/SendMessageMiddleware.php(73): Middleware\\HandleMessageMiddleware->handle()\n#13 /vendor/symfony/messenger/Middleware/FailedMessageProcessingMiddleware.php(34): Middleware\\SendMessageMiddleware->handle()\n#14 /vendor/symfony/messenger/Middleware/DispatchAfterCurrentBusMiddleware.php(68): Middleware\\FailedMessageProcessingMiddleware->handle()\n#15 /vendor/symfony/messenger/Middleware/RejectRedeliveredMessageMiddleware.php(48): Middleware\\DispatchAfterCurrentBusMiddleware->handle()\n#16 /vendor/symfony/messenger/Middleware/AddBusNameStampMiddleware.php(37): Middleware\\RejectRedeliveredMessageMiddleware->handle()\n#17 /vendor/symfony/messenger/Middleware/TraceableMiddleware.php(43): Middleware\\AddBusNameStampMiddleware->handle()\n#18 /vendor/symfony/messenger/MessageBus.php(77): Messenger\\Middleware\\TraceableMiddleware->handle()\n#19 /vendor/symfony/messenger/HandleTrait.php(43): Messenger\\MessageBus->dispatch()\n#20 /vendor/cqrs/Bus/QueryBus.php(36): CQRS\\Bus\\QueryBus->handleQuery()\n#21 /vendor/cqrs/System/Middleware/HandleQueryMiddleware.php(34): CQRS\\Bus\\QueryBus->handle()\n#22 /vendor/cqrs/System/System.php(94): CQRS\\System\\Middleware\\HandleQueryMiddleware->handle()\n#23 /vendor/cqrs/System/System.php(100): CQRS\\System\\System->{closure}()\n#24 /vendor/cqrs/System/System.php(58): CQRS\\System\\System->handleMessage()\n#25 /src/Controller/ProductController.php(50): CQRS\\System\\System->query()\n#26 /vendor/symfony/http-kernel/HttpKernel.php(152): Controller\\ProductController->getPlans()\n#27 /vendor/symfony/http-kernel/HttpKernel.php(74): HttpKernel->handleRaw()\n#28 /vendor/symfony/http-kernel/Kernel.php(202): HttpKernel->handle()\n#29 /public/index.php(30): Kernel->handle()\n#30 {main}"
        }
    ]
}
Originally created by @pizgariu on GitHub (Dec 24, 2024). ### Bug Report | Q | A |-------------------------------------------- | ------ | Version | 2.12.2 | Previous Version if the bug is a regression | Not applicable #### Summary When using a custom DBAL type that converts JSON data into a `Doctrine\Common\Collections\Collection` (e.g., `ArrayCollection`), Doctrine fails to properly handle hydration. Despite the type being registered and used in the entity, the `convertToPHPValue` method of the custom type is ignored during hydration, resulting in a `TypeError`. Doctrine tries to assign an `array` to a property typed as `Collection`, which leads to the error. #### Current behavior Doctrine does not use the custom DBAL type during hydration. Instead, it attempts to assign a raw `array` to the entity property, ignoring the expected conversion to `Collection`. #### Expected behavior Doctrine should respect the custom DBAL type and correctly convert the JSON data into the expected `Collection` instance during hydration. #### How to reproduce Provide a custom DBAL type: ```php namespace App\DBAL\Types; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type; use Doctrine\Common\Collections\ArrayCollection; class JsonCollectionType extends Type { const JSON_COLLECTION = 'json_collection'; public function getSQLDeclaration(array $column, AbstractPlatform $platform) { return $platform->getJsonTypeDeclarationSQL($column); } public function convertToPHPValue($value, AbstractPlatform $platform) { $data = json_decode($value, true); return new ArrayCollection($data ?? []); } public function convertToDatabaseValue($value, AbstractPlatform $platform) { if ($value instanceof ArrayCollection) { $value = $value->toArray(); } return json_encode($value); } public function getName(): string { return self::JSON_COLLECTION; } } ``` Register the type: ```php use Doctrine\DBAL\Types\Type; if (!Type::hasType('json_collection')) { Type::addType('json_collection', \App\DBAL\Types\JsonCollectionType::class); } ``` Define an entity: ```php use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\ArrayCollection; #[ORM\Entity] class Product { #[ORM\Column(type: "json_collection")] private Collection $paymentMethodsUuids; public function __construct() { $this->paymentMethodsUuids = new ArrayCollection(); } public function getPaymentMethodsUuids(): Collection { return $this->paymentMethodsUuids; } public function setPaymentMethodsUuids(Collection $paymentMethodsUuids): void { $this->paymentMethodsUuids = $paymentMethodsUuids; } } ``` When trying to hydrate this entity, you get the following error: ``` TypeError: Typed property App\Entity\Product::$paymentMethodsUuids must be an instance of Doctrine\Common\Collections\Collection, array used ``` #### Stacktrace <details> <summary>Click to expand stacktrace</summary> ``` { "errors": [ { "class": "TypeError", "message": "Typed property Entity::$paymentMethodsUuids must be an instance of Collection, array used", "code": 0, "file": "/vendor/doctrine/persistence/src/Persistence/Reflection/TypedNoDefaultReflectionPropertyBase.php", "line": 64, "stacktrace": "#0 /vendor/doctrine/persistence/src/Persistence/Reflection/TypedNoDefaultReflectionPropertyBase.php(64): ReflectionProperty->setValue()\n#1 /vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php(2745): Persistence\\Reflection\\TypedNoDefaultReflectionProperty->setValue()\n#2 /vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php(255): ORM\\UnitOfWork->createEntity()\n#3 /vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php(481): ORM\\Internal\\Hydration\\ObjectHydrator->getEntity()\n#4 /vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php(143): ORM\\Internal\\Hydration\\ObjectHydrator->hydrateRowData()\n#5 /vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php(270): ORM\\Internal\\Hydration\\ObjectHydrator->hydrateAllData()\n#6 /vendor/doctrine/orm/lib/Doctrine/ORM/AbstractQuery.php(1201): ORM\\Internal\\Hydration\\AbstractHydrator->hydrateAll()\n#7 /vendor/doctrine/orm/lib/Doctrine/ORM/AbstractQuery.php(1142): ORM\\AbstractQuery->executeIgnoreQueryCache()\n#8 /vendor/doctrine/orm/lib/Doctrine/ORM/AbstractQuery.php(878): ORM\\AbstractQuery->execute()\n#9 /src/Repository/ProductRepository.php(137): ORM\\AbstractQuery->getResult()\n#10 /src/Handler/Query/GetActiveProductQueryHandler.php(18): Repository\\ProductRepository->findActivePlans()\n#11 /vendor/symfony/messenger/Middleware/HandleMessageMiddleware.php(95): Handler\\Query\\GetActiveProductQueryHandler->__invoke()\n#12 /vendor/symfony/messenger/Middleware/SendMessageMiddleware.php(73): Middleware\\HandleMessageMiddleware->handle()\n#13 /vendor/symfony/messenger/Middleware/FailedMessageProcessingMiddleware.php(34): Middleware\\SendMessageMiddleware->handle()\n#14 /vendor/symfony/messenger/Middleware/DispatchAfterCurrentBusMiddleware.php(68): Middleware\\FailedMessageProcessingMiddleware->handle()\n#15 /vendor/symfony/messenger/Middleware/RejectRedeliveredMessageMiddleware.php(48): Middleware\\DispatchAfterCurrentBusMiddleware->handle()\n#16 /vendor/symfony/messenger/Middleware/AddBusNameStampMiddleware.php(37): Middleware\\RejectRedeliveredMessageMiddleware->handle()\n#17 /vendor/symfony/messenger/Middleware/TraceableMiddleware.php(43): Middleware\\AddBusNameStampMiddleware->handle()\n#18 /vendor/symfony/messenger/MessageBus.php(77): Messenger\\Middleware\\TraceableMiddleware->handle()\n#19 /vendor/symfony/messenger/HandleTrait.php(43): Messenger\\MessageBus->dispatch()\n#20 /vendor/cqrs/Bus/QueryBus.php(36): CQRS\\Bus\\QueryBus->handleQuery()\n#21 /vendor/cqrs/System/Middleware/HandleQueryMiddleware.php(34): CQRS\\Bus\\QueryBus->handle()\n#22 /vendor/cqrs/System/System.php(94): CQRS\\System\\Middleware\\HandleQueryMiddleware->handle()\n#23 /vendor/cqrs/System/System.php(100): CQRS\\System\\System->{closure}()\n#24 /vendor/cqrs/System/System.php(58): CQRS\\System\\System->handleMessage()\n#25 /src/Controller/ProductController.php(50): CQRS\\System\\System->query()\n#26 /vendor/symfony/http-kernel/HttpKernel.php(152): Controller\\ProductController->getPlans()\n#27 /vendor/symfony/http-kernel/HttpKernel.php(74): HttpKernel->handleRaw()\n#28 /vendor/symfony/http-kernel/Kernel.php(202): HttpKernel->handle()\n#29 /public/index.php(30): Kernel->handle()\n#30 {main}" } ] } ``` </details>
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#7453