DDC-1640: Actual classMetadata is not used when entity with DiscriminatorMap is hydrated, instead AbstractClass metaData is used. #2060

Closed
opened 2026-01-22 13:39:06 +01:00 by admin · 4 comments
Owner

Originally created by @doctrinebot on GitHub (Feb 8, 2012).

Originally assigned to: @beberlei on GitHub.

Jira issue originally created by user jelte@marlon:

Because the className changes in ObjectHydrator:226 it might be that the ClassMetadata has not been loaded yet.
When the classMetadata has not been loaded the PostLoad events are not executed.

adding the following after the className has changed solves the issue.

$this->_getClassMetadata($className);

example:

Class Session {
    /****
     * @access private
     * @var ArrayCollection
     * @ORM\OneToMany(targetEntity="AbstractService", mappedBy="session")
     */
    public $services;
}
/****
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 * @ORM\InheritanceType("SINGLE_TABLE")
 * @ORM\DiscriminatorColumn(name="type", type="string")
 * @ORM\DiscriminatorMap({
 *      "A" = "ServiceA",
 *      "B" = "ServiceB"
 * })
 */
abstract class AbstractService 
{
    /****
     * @access private
     * @var string
     * @ORM\Column()
     */
    private $_status;

    public $status;

    /****
     * @access public
     * @internal Do not use; this is for Doctrine only
     * @ORM\PostLoad
     * @return void
     */
    public function _reconstituteValueObjects()
    {
        $this->status = new Status($this->_status);
    }
}

When I would do now:

foreach ( $session->services as $service ) {
    $service->status->stop();
}

$service would be a concrete class ServceA or ServiceB, but these ClassMetadata's might not be loaded yet. If this is the cause then $service->status will be null as the @PostLoad has not been executed.

Originally created by @doctrinebot on GitHub (Feb 8, 2012). Originally assigned to: @beberlei on GitHub. Jira issue originally created by user jelte@marlon: Because the className changes in ObjectHydrator:226 it might be that the ClassMetadata has not been loaded yet. When the classMetadata has not been loaded the PostLoad events are not executed. adding the following after the className has changed solves the issue. ``` $this->_getClassMetadata($className); ``` example: ``` Class Session { /**** * @access private * @var ArrayCollection * @ORM\OneToMany(targetEntity="AbstractService", mappedBy="session") */ public $services; } ``` ``` /**** * @ORM\Entity * @ORM\HasLifecycleCallbacks * @ORM\InheritanceType("SINGLE_TABLE") * @ORM\DiscriminatorColumn(name="type", type="string") * @ORM\DiscriminatorMap({ * "A" = "ServiceA", * "B" = "ServiceB" * }) */ abstract class AbstractService { /**** * @access private * @var string * @ORM\Column() */ private $_status; public $status; /**** * @access public * @internal Do not use; this is for Doctrine only * @ORM\PostLoad * @return void */ public function _reconstituteValueObjects() { $this->status = new Status($this->_status); } } ``` When I would do now: ``` foreach ( $session->services as $service ) { $service->status->stop(); } ``` $service would be a concrete class ServceA or ServiceB, but these ClassMetadata's might not be loaded yet. If this is the cause then $service->status will be null as the @PostLoad has not been executed.
admin added the Bug label 2026-01-22 13:39:06 +01:00
admin closed this issue 2026-01-22 13:39:06 +01:00
Author
Owner

@doctrinebot commented on GitHub (Feb 9, 2012):

Comment created by jelte@marlon:

I've have found the same issue with the SimpleObjectHydrator.

Although here it might have worse consequences.
When a row is hydrated it the class has a discriminatorMap, the classMetadata of the defined class (usually the abstract class) is used and not the actual classMetadata of the class it will be converted to.
Same issue here that the @PostLoad isn't triggered but also

$this->registerManaged($this->class,...) 

uses the wrong classMetadata is used.

should be: Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator:86

    protected function hydrateRowData(array $sqlResult, array &$cache, array &$result)
    {
        $classMetadata = $this->class;
        $entityName = $this->class->name;
        $data       = array();

        // We need to find the correct entity class name if we have inheritance in resultset
        if ($classMetadata->inheritanceType !== ClassMetadata::INHERITANCE*TYPE*NONE) {
            $discrColumnName = $this->_platform->getSQLResultCasing($classMetadata->discriminatorColumn['name']);

            if ($sqlResult[$discrColumnName] === '') {
                throw HydrationException::emptyDiscriminatorValue(key($this->_rsm->aliasMap));
            }

            $entityName = $classMetadata->discriminatorMap[$sqlResult[$discrColumnName]];
            $classMetadata = $this->_getClassMetadata($entityName);

            unset($sqlResult[$discrColumnName]);
        }
        foreach ($sqlResult as $column => $value) {
            // Hydrate column information if not yet present
            if ( ! isset($cache[$column])) {
                if (($info = $this->hydrateColumnInfo($entityName, $column)) === null) {
                    continue;
                }

                $cache[$column] = $info;
            }

            // Convert field to a valid PHP value
            if (isset($cache[$column]['field'])) {
                $type  = Type::getType($cache[$column]['class']->fieldMappings[$cache[$column]['name']]['type']);
                $value = $type->convertToPHPValue($value, $this->_platform);
            }

            // Prevent overwrite in case of inherit classes using same property name (See AbstractHydrator)
            if (isset($cache[$column]) && ( ! isset($data[$cache[$column]['name']]) || $value !== null)) {
                $data[$cache[$column]['name']] = $value;
            }
        }

        if (isset($this->*hints[Query::HINT_REFRESH*ENTITY])) {
            $this->registerManaged($classMetadata, $this->*hints[Query::HINT_REFRESH*ENTITY], $data);
        }

        $uow    = $this->_em->getUnitOfWork();
        $entity = $uow->createEntity($entityName, $data, $this->_hints);

        //TODO: These should be invoked later, after hydration, because associations may not yet be loaded here.
        if (isset($classMetadata->lifecycleCallbacks[Events::postLoad])) {
            $classMetadata->invokeLifecycleCallbacks(Events::postLoad, $entity);
        }

        $evm = $this->_em->getEventManager();

        if ($evm->hasListeners(Events::postLoad)) {
            $evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity, $this->_em));
        }

        $result[] = $entity;
    }
@doctrinebot commented on GitHub (Feb 9, 2012): Comment created by jelte@marlon: I've have found the same issue with the SimpleObjectHydrator. Although here it might have worse consequences. When a row is hydrated it the class has a discriminatorMap, the classMetadata of the defined class (usually the abstract class) is used and not the actual classMetadata of the class it will be converted to. Same issue here that the @PostLoad isn't triggered but also ``` $this->registerManaged($this->class,...) ``` uses the wrong classMetadata is used. should be: Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator:86 ``` protected function hydrateRowData(array $sqlResult, array &$cache, array &$result) { $classMetadata = $this->class; $entityName = $this->class->name; $data = array(); // We need to find the correct entity class name if we have inheritance in resultset if ($classMetadata->inheritanceType !== ClassMetadata::INHERITANCE*TYPE*NONE) { $discrColumnName = $this->_platform->getSQLResultCasing($classMetadata->discriminatorColumn['name']); if ($sqlResult[$discrColumnName] === '') { throw HydrationException::emptyDiscriminatorValue(key($this->_rsm->aliasMap)); } $entityName = $classMetadata->discriminatorMap[$sqlResult[$discrColumnName]]; $classMetadata = $this->_getClassMetadata($entityName); unset($sqlResult[$discrColumnName]); } foreach ($sqlResult as $column => $value) { // Hydrate column information if not yet present if ( ! isset($cache[$column])) { if (($info = $this->hydrateColumnInfo($entityName, $column)) === null) { continue; } $cache[$column] = $info; } // Convert field to a valid PHP value if (isset($cache[$column]['field'])) { $type = Type::getType($cache[$column]['class']->fieldMappings[$cache[$column]['name']]['type']); $value = $type->convertToPHPValue($value, $this->_platform); } // Prevent overwrite in case of inherit classes using same property name (See AbstractHydrator) if (isset($cache[$column]) && ( ! isset($data[$cache[$column]['name']]) || $value !== null)) { $data[$cache[$column]['name']] = $value; } } if (isset($this->*hints[Query::HINT_REFRESH*ENTITY])) { $this->registerManaged($classMetadata, $this->*hints[Query::HINT_REFRESH*ENTITY], $data); } $uow = $this->_em->getUnitOfWork(); $entity = $uow->createEntity($entityName, $data, $this->_hints); //TODO: These should be invoked later, after hydration, because associations may not yet be loaded here. if (isset($classMetadata->lifecycleCallbacks[Events::postLoad])) { $classMetadata->invokeLifecycleCallbacks(Events::postLoad, $entity); } $evm = $this->_em->getEventManager(); if ($evm->hasListeners(Events::postLoad)) { $evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity, $this->_em)); } $result[] = $entity; } ```
Author
Owner

@doctrinebot commented on GitHub (Feb 9, 2012):

Comment created by jelte@marlon:

Changed summery to better reflect the bug.

@doctrinebot commented on GitHub (Feb 9, 2012): Comment created by jelte@marlon: Changed summery to better reflect the bug.
Author
Owner

@doctrinebot commented on GitHub (Feb 17, 2012):

Comment created by @beberlei:

Fixed.

@doctrinebot commented on GitHub (Feb 17, 2012): Comment created by @beberlei: Fixed.
Author
Owner

@doctrinebot commented on GitHub (Feb 17, 2012):

Issue was closed with resolution "Fixed"

@doctrinebot commented on GitHub (Feb 17, 2012): Issue was closed with resolution "Fixed"
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#2060