Incorrect work with custom type in class table inheritance #6152

Open
opened 2026-01-22 15:27:49 +01:00 by admin · 9 comments
Owner

Originally created by @voodoo-dn on GitHub (Jan 9, 2019).

Originally assigned to: @voodoo-dn on GitHub.

Bug Report

App\Domain\Payment\Model\AbstractOrder:
    type: entity
    repositoryClass: App\Domain\Payment\Repository\OrderRepository
    table: payment_orders
    inheritanceType: JOINED
    discriminatorColumn:
        name: type
        type: string
    discriminatorMap:
        card: App\Domain\Payment\Model\CardOrder
        withdraw: App\Domain\Payment\Model\WithdrawOrder
    id:
        id:
            type: uuid
            unique: true
    fields:
        operation:
            type: order_operation
            nullable: false
        status:
            type: order_status
            nullable: false
        description:
            type: string
            nullable: true
        createdAt:
            type: datetime_immutable
            nullable: false
        updatedAt:
            type: datetime_immutable
            nullable: true
        clientIp:
            type: string
            nullable: true
        clientUserAgent:
            type: string
            nullable: true
    manyToOne:
        user:
            targetEntity: App\Domain\User\UserCore\Model\User
            onDelete: CASCADE
        paymentSystem:
            targetEntity: App\Domain\Payment\Model\PaymentSystem
            onDelete: CASCADE
App\Domain\Payment\Model\CardOrder:
    type: entity
    table: payment_card_orders
    fields:
        amount:
            type: decimal
            precious: 10
            scale: 2
            nullable: false
    manyToOne:
        currency:
            targetEntity: App\Domain\Payment\Model\Currency
            onDelete: CASCADE
App\Domain\Payment\Model\WithdrawOrder:
    type: entity
    table: payment_withdraw_orders
    fields:
        amount:
            type: decimal
            precious: 10
            scale: 2
            nullable: false
        confirmationStatus:
            type: withdraw_order_confirmation_status
            nullable: false
    manyToOne:
        currency:
            targetEntity: App\Domain\Payment\Model\Currency
            onDelete: CASCADE
        confirmedBy:
            targetEntity: App\Domain\User\UserCore\Model\User
            onDelete: CASCADE
<?php

declare(strict_types=1);

namespace App\Representation\Database\Payment\Doctrine\DBAL\Type;

use App\Domain\Payment\Model\ConfirmationStatus;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;

class ConfirmationStatusType extends Type
{
    /**
     * @param mixed            $value
     * @param AbstractPlatform $platform
     *
     * @return string
     *
     * @throws ConversionException
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform): string
    {
        if (!$value instanceof ConfirmationStatus) {
            throw new ConversionException(\sprintf(
                'Value must be instance of "%s", instance "%s" given',
                ConfirmationStatus::class,
                \is_object($value) ? \get_class($value) : \gettype($value)
            ));
        }

        /* @var ConfirmationStatus $value */
        return $value->getValue();
    }

    /**
     * @param mixed            $value
     * @param AbstractPlatform $platform
     *
     * @return ConfirmationStatus
     *
     * @throws ConversionException
     */
    public function convertToPHPValue($value, AbstractPlatform $platform): ConfirmationStatus
    {
        try {
            return new ConfirmationStatus($value);
        } catch (\Throwable $e) {
            throw new ConversionException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * @param array            $fieldDeclaration
     * @param AbstractPlatform $platform
     *
     * @return string
     */
    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
    {
        return 'withdraw_order_confirmation_status';
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return 'withdraw_order_confirmation_status';
    }
}

When I fetch order by ID, CardOrder must be returned. But in Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator::hydrateRowData in $sqlResult argument passed array with columns from WithdrawOrder(e.g. confirmation_status = null, confirmed_by = null), and Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator::hydrateRowData(row 130), throws exception, because my custom doctrine type not expects null value.

Q A
BC Break yes/no
Version 2.6.2
Originally created by @voodoo-dn on GitHub (Jan 9, 2019). Originally assigned to: @voodoo-dn on GitHub. ### Bug Report ```yml App\Domain\Payment\Model\AbstractOrder: type: entity repositoryClass: App\Domain\Payment\Repository\OrderRepository table: payment_orders inheritanceType: JOINED discriminatorColumn: name: type type: string discriminatorMap: card: App\Domain\Payment\Model\CardOrder withdraw: App\Domain\Payment\Model\WithdrawOrder id: id: type: uuid unique: true fields: operation: type: order_operation nullable: false status: type: order_status nullable: false description: type: string nullable: true createdAt: type: datetime_immutable nullable: false updatedAt: type: datetime_immutable nullable: true clientIp: type: string nullable: true clientUserAgent: type: string nullable: true manyToOne: user: targetEntity: App\Domain\User\UserCore\Model\User onDelete: CASCADE paymentSystem: targetEntity: App\Domain\Payment\Model\PaymentSystem onDelete: CASCADE ``` ```yml App\Domain\Payment\Model\CardOrder: type: entity table: payment_card_orders fields: amount: type: decimal precious: 10 scale: 2 nullable: false manyToOne: currency: targetEntity: App\Domain\Payment\Model\Currency onDelete: CASCADE ``` ```yml App\Domain\Payment\Model\WithdrawOrder: type: entity table: payment_withdraw_orders fields: amount: type: decimal precious: 10 scale: 2 nullable: false confirmationStatus: type: withdraw_order_confirmation_status nullable: false manyToOne: currency: targetEntity: App\Domain\Payment\Model\Currency onDelete: CASCADE confirmedBy: targetEntity: App\Domain\User\UserCore\Model\User onDelete: CASCADE ``` ```php <?php declare(strict_types=1); namespace App\Representation\Database\Payment\Doctrine\DBAL\Type; use App\Domain\Payment\Model\ConfirmationStatus; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; class ConfirmationStatusType extends Type { /** * @param mixed $value * @param AbstractPlatform $platform * * @return string * * @throws ConversionException */ public function convertToDatabaseValue($value, AbstractPlatform $platform): string { if (!$value instanceof ConfirmationStatus) { throw new ConversionException(\sprintf( 'Value must be instance of "%s", instance "%s" given', ConfirmationStatus::class, \is_object($value) ? \get_class($value) : \gettype($value) )); } /* @var ConfirmationStatus $value */ return $value->getValue(); } /** * @param mixed $value * @param AbstractPlatform $platform * * @return ConfirmationStatus * * @throws ConversionException */ public function convertToPHPValue($value, AbstractPlatform $platform): ConfirmationStatus { try { return new ConfirmationStatus($value); } catch (\Throwable $e) { throw new ConversionException($e->getMessage(), $e->getCode(), $e); } } /** * @param array $fieldDeclaration * @param AbstractPlatform $platform * * @return string */ public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string { return 'withdraw_order_confirmation_status'; } /** * @return string */ public function getName(): string { return 'withdraw_order_confirmation_status'; } } ``` When I fetch order by ID, CardOrder must be returned. But in Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator::hydrateRowData in $sqlResult argument passed array with columns from WithdrawOrder(e.g. confirmation_status = null, confirmed_by = null), and Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator::hydrateRowData(row 130), throws exception, because my custom doctrine type not expects null value. | Q | A |------------ | ------ | BC Break | yes/no | Version | 2.6.2
admin added the Missing TestsQuestion labels 2026-01-22 15:27:49 +01:00
Author
Owner

@Ocramius commented on GitHub (Jan 9, 2019):

What exception?

@Ocramius commented on GitHub (Jan 9, 2019): What exception?
Author
Owner

@voodoo-dn commented on GitHub (Jan 9, 2019):

What exception?

\Doctrine\DBAL\Types\ConversionException

Because to table payment_orders joined payment_card_orders and payment_withdraw_orders, columns confirmation_status, confirmed_by contains null value. ConfirmationStatusType:: convertToPHPValue() do not expects null, it expects string(because ConfirmationStatus object is enum, which expected pending, approved, declined values).

I MUST receive App\Domain\Payment\Model\CardOrder object from repository from tables payment_orders + payment_card_orders.

@voodoo-dn commented on GitHub (Jan 9, 2019): > What exception? \Doctrine\DBAL\Types\ConversionException Because to table `payment_orders` joined `payment_card_orders` and `payment_withdraw_orders`, columns `confirmation_status`, `confirmed_by` contains `null` value. ConfirmationStatusType:: convertToPHPValue() do not expects null, it expects string(because ConfirmationStatus object is enum, which expected `pending`, `approved`, `declined` values). I MUST receive App\Domain\Payment\Model\CardOrder object from repository from tables `payment_orders` + `payment_card_orders`.
Author
Owner

@Ocramius commented on GitHub (Jan 9, 2019):

I'd check first:

  • what SQL is being generated for the query that fails
  • what the resultset for said query is

This would isolate the issue to either the persisters or the hydrators at first.

@Ocramius commented on GitHub (Jan 9, 2019): I'd check first: * what SQL is being generated for the query that fails * what the resultset for said query is This would isolate the issue to either the persisters or the hydrators at first.
Author
Owner

@voodoo-dn commented on GitHub (Jan 9, 2019):

SQL: SELECT t0.id AS id_3, t0.operation AS operation_4, t0.status AS status_5, t0.description AS description_6, t0.created_at AS created_at_7, t0.updated_at AS updated_at_8, t0.client_ip AS client_ip_9, t0.client_user_agent AS client_user_agent_10, t0.user_id AS user_id_11, t0.payment_system_id AS payment_system_id_12, t0.type, t1.amount AS amount_13, t1.currency_id AS currency_id_14, t2.amount AS amount_15, t2.confirmation_status AS confirmation_status_16, t2.currency_id AS currency_id_17, t2.confirmed_by_id AS confirmed_by_id_18 FROM payment_orders t0 LEFT JOIN payment_card_orders t1 ON t0.id = t1.id LEFT JOIN payment_withdraw_orders t2 ON t0.id = t2.id WHERE t0.id = ?

Result set(array dump to json):

{
   "id_3":"a45212a4-808f-4b02-bbbd-fae9040c4041",
   "operation_4":"in",
   "status_5":"pending",
   "description_6":"",
   "created_at_7":"2019-01-09 17:19:15",
   "updated_at_8":null,
   "client_ip_9":"",
   "client_user_agent_10":"",
   "user_id_11":"976c3f40-d774-495f-a8e2-9662e354927b",
   "payment_system_id_12":"c4a886f3-239c-4cd1-8462-1bee07937c2b",
   "type":"card",
   "amount_13":"1000.00",
   "currency_id_14":8,
   "amount_15":null,
   "confirmation_status_16":null,
   "currency_id_17":null,
   "confirmed_by_id_18":null
}
@voodoo-dn commented on GitHub (Jan 9, 2019): SQL: `SELECT t0.id AS id_3, t0.operation AS operation_4, t0.status AS status_5, t0.description AS description_6, t0.created_at AS created_at_7, t0.updated_at AS updated_at_8, t0.client_ip AS client_ip_9, t0.client_user_agent AS client_user_agent_10, t0.user_id AS user_id_11, t0.payment_system_id AS payment_system_id_12, t0.type, t1.amount AS amount_13, t1.currency_id AS currency_id_14, t2.amount AS amount_15, t2.confirmation_status AS confirmation_status_16, t2.currency_id AS currency_id_17, t2.confirmed_by_id AS confirmed_by_id_18 FROM payment_orders t0 LEFT JOIN payment_card_orders t1 ON t0.id = t1.id LEFT JOIN payment_withdraw_orders t2 ON t0.id = t2.id WHERE t0.id = ?` Result set(array dump to json): ``` { "id_3":"a45212a4-808f-4b02-bbbd-fae9040c4041", "operation_4":"in", "status_5":"pending", "description_6":"", "created_at_7":"2019-01-09 17:19:15", "updated_at_8":null, "client_ip_9":"", "client_user_agent_10":"", "user_id_11":"976c3f40-d774-495f-a8e2-9662e354927b", "payment_system_id_12":"c4a886f3-239c-4cd1-8462-1bee07937c2b", "type":"card", "amount_13":"1000.00", "currency_id_14":8, "amount_15":null, "confirmation_status_16":null, "currency_id_17":null, "confirmed_by_id_18":null } ```
Author
Owner

@Ocramius commented on GitHub (Jan 9, 2019):

Hmm, so the status_5 is correctly set. The confirmation_status_16 is correctly null due to JTI, and resultset rows are correctly transformed according to the corresponding DBAL type mappings.

What can be done here is deferring DBAL type conversions until the column is effectively needed, instead of when the column is iterated upon on the row.

Would you be able to turn this into a minimal test case in https://github.com/doctrine/doctrine2/tree/master/tests/Doctrine/Tests/ORM/Functional/Ticket ?

@Ocramius commented on GitHub (Jan 9, 2019): Hmm, so the `status_5` is correctly set. The `confirmation_status_16` is correctly `null` due to JTI, and resultset rows are correctly transformed according to the corresponding DBAL type mappings. What can be done here is deferring DBAL type conversions until the column is effectively needed, instead of when the column is iterated upon on the row. Would you be able to turn this into a minimal test case in https://github.com/doctrine/doctrine2/tree/master/tests/Doctrine/Tests/ORM/Functional/Ticket ?
Author
Owner

@voodoo-dn commented on GitHub (Jan 9, 2019):

Would you be able to turn this into a minimal test case in https://github.com/doctrine/doctrine2/tree/master/tests/Doctrine/Tests/ORM/Functional/Ticket ?

Thanks for the answer. I will try.

@voodoo-dn commented on GitHub (Jan 9, 2019): > Would you be able to turn this into a minimal test case in https://github.com/doctrine/doctrine2/tree/master/tests/Doctrine/Tests/ORM/Functional/Ticket ? Thanks for the answer. I will try.
Author
Owner

@voodoo-dn commented on GitHub (Jan 10, 2019):

How I can commit test? Or I need attach it to comment?

@voodoo-dn commented on GitHub (Jan 10, 2019): How I can commit test? Or I need attach it to comment?
Author
Owner

@Ocramius commented on GitHub (Jan 10, 2019):

Create a new branch from 2.6 in this repository, then push it to your own fork and open a pull request. See https://help.github.com/articles/creating-a-pull-request/

@Ocramius commented on GitHub (Jan 10, 2019): Create a new branch from `2.6` in this repository, then push it to your own fork and open a pull request. See https://help.github.com/articles/creating-a-pull-request/
Author
Owner

@voodoo-dn commented on GitHub (Jan 10, 2019):

@Ocramius, thanks. Test writed. https://github.com/doctrine/orm/pull/7566

@voodoo-dn commented on GitHub (Jan 10, 2019): @Ocramius, thanks. Test writed. https://github.com/doctrine/orm/pull/7566
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#6152