Doctrine 2 - ORM
  1. Doctrine 2 - ORM
  2. DDC-1640

Actual classMetadata is not used when entity with DiscriminatorMap is hydrated, instead AbstractClass metaData is used.

    Details

    • Type: Bug Bug
    • Status: Resolved
    • Priority: Critical Critical
    • Resolution: Fixed
    • Affects Version/s: 2.2
    • Fix Version/s: 2.2.1
    • Component/s: ORM
    • Security Level: All
    • Labels:
      None

      Description

      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.

        Activity

        Jelte Steijaert created issue -
        Hide
        Jelte Steijaert added a comment - - edited

        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;
            }
        
        Show
        Jelte Steijaert added a comment - - edited 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; }
        Hide
        Jelte Steijaert added a comment -

        Changed summery to better reflect the bug.

        Show
        Jelte Steijaert added a comment - Changed summery to better reflect the bug.
        Jelte Steijaert made changes -
        Field Original Value New Value
        Summary @PostLoad event not triggered when entity loaded through ObjectHydrator Actual classMetadata is not used when entity with DiscriminatorMap is hydrated, instead AbstractClass metaData is used.
        Priority Major [ 3 ] Critical [ 2 ]
        Hide
        Benjamin Eberlei added a comment -

        Fixed.

        Show
        Benjamin Eberlei added a comment - Fixed.
        Benjamin Eberlei made changes -
        Status Open [ 1 ] Resolved [ 5 ]
        Fix Version/s 2.2.1 [ 10194 ]
        Resolution Fixed [ 1 ]
        Benjamin Eberlei made changes -
        Workflow jira [ 13425 ] jira-feedback [ 15222 ]
        Benjamin Eberlei made changes -
        Workflow jira-feedback [ 15222 ] jira-feedback2 [ 17086 ]
        Benjamin Eberlei made changes -
        Workflow jira-feedback2 [ 17086 ] jira-feedback3 [ 19339 ]

        This list may be incomplete, as errors occurred whilst retrieving source from linked applications:

        • Request to http://www.doctrine-project.org/fisheye/ failed: Error in remote call to 'FishEye 0 (http://www.doctrine-project.org/fisheye/)' (http://www.doctrine-project.org/fisheye) [AbstractRestCommand{path='/rest-service-fe/search-v1/crossRepositoryQuery', params={query=DDC-1640, expand=changesets[0:20].revisions[0:29],reviews}, methodType=GET}] : Received status code 503 (Service Temporarily Unavailable)

          People

          • Assignee:
            Benjamin Eberlei
            Reporter:
            Jelte Steijaert
          • Votes:
            0 Vote for this issue
            Watchers:
            0 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved: