vendor/doctrine/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php
changeset 0 7f95f8617b0b
equal deleted inserted replaced
-1:000000000000 0:7f95f8617b0b
       
     1 <?php
       
     2 /*
       
     3  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
       
     4  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
       
     5  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
       
     6  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
       
     7  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
       
     8  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
       
     9  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
       
    10  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
       
    11  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
       
    12  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
       
    13  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
       
    14  *
       
    15  * This software consists of voluntary contributions made by many individuals
       
    16  * and is licensed under the LGPL. For more information, see
       
    17  * <http://www.doctrine-project.org>.
       
    18  */
       
    19 
       
    20 namespace Doctrine\ORM\Internal\Hydration;
       
    21 
       
    22 use PDO,
       
    23     Doctrine\ORM\Mapping\ClassMetadata,
       
    24     Doctrine\ORM\PersistentCollection,
       
    25     Doctrine\ORM\Query,
       
    26     Doctrine\Common\Collections\ArrayCollection,
       
    27     Doctrine\Common\Collections\Collection;
       
    28 
       
    29 /**
       
    30  * The ObjectHydrator constructs an object graph out of an SQL result set.
       
    31  *
       
    32  * @author Roman Borschel <roman@code-factory.org>
       
    33  * @since 2.0
       
    34  * @internal Highly performance-sensitive code.
       
    35  */
       
    36 class ObjectHydrator extends AbstractHydrator
       
    37 {
       
    38     /* Local ClassMetadata cache to avoid going to the EntityManager all the time.
       
    39      * This local cache is maintained between hydration runs and not cleared.
       
    40      */
       
    41     private $_ce = array();
       
    42 
       
    43     /* The following parts are reinitialized on every hydration run. */
       
    44 
       
    45     private $_identifierMap;
       
    46     private $_resultPointers;
       
    47     private $_idTemplate;
       
    48     private $_resultCounter;
       
    49     private $_rootAliases = array();
       
    50     private $_initializedCollections = array();
       
    51     private $_existingCollections = array();
       
    52     //private $_createdEntities;
       
    53 
       
    54 
       
    55     /** @override */
       
    56     protected function _prepare()
       
    57     {
       
    58         $this->_identifierMap =
       
    59         $this->_resultPointers =
       
    60         $this->_idTemplate = array();
       
    61         $this->_resultCounter = 0;
       
    62         if (!isset($this->_hints['deferEagerLoad'])) {
       
    63             $this->_hints['deferEagerLoad'] = true;
       
    64         }
       
    65         
       
    66         foreach ($this->_rsm->aliasMap as $dqlAlias => $className) {
       
    67             $this->_identifierMap[$dqlAlias] = array();
       
    68             $this->_idTemplate[$dqlAlias] = '';
       
    69             $class = $this->_em->getClassMetadata($className);
       
    70 
       
    71             if ( ! isset($this->_ce[$className])) {
       
    72                 $this->_ce[$className] = $class;
       
    73             }
       
    74 
       
    75             // Remember which associations are "fetch joined", so that we know where to inject
       
    76             // collection stubs or proxies and where not.
       
    77             if (isset($this->_rsm->relationMap[$dqlAlias])) {
       
    78                 if ( ! isset($this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]])) {
       
    79                     throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $this->_rsm->parentAliasMap[$dqlAlias]);
       
    80                 }
       
    81 
       
    82                 $sourceClassName = $this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]];
       
    83                 $sourceClass = $this->_getClassMetadata($sourceClassName);
       
    84                 $assoc = $sourceClass->associationMappings[$this->_rsm->relationMap[$dqlAlias]];
       
    85                 $this->_hints['fetched'][$sourceClassName][$assoc['fieldName']] = true;
       
    86                 if ($sourceClass->subClasses) {
       
    87                     foreach ($sourceClass->subClasses as $sourceSubclassName) {
       
    88                         $this->_hints['fetched'][$sourceSubclassName][$assoc['fieldName']] = true;
       
    89                     }
       
    90                 }
       
    91                 if ($assoc['type'] != ClassMetadata::MANY_TO_MANY) {
       
    92                     // Mark any non-collection opposite sides as fetched, too.
       
    93                     if ($assoc['mappedBy']) {
       
    94                         $this->_hints['fetched'][$className][$assoc['mappedBy']] = true;
       
    95                     } else {
       
    96                         if ($assoc['inversedBy']) {
       
    97                             $inverseAssoc = $class->associationMappings[$assoc['inversedBy']];
       
    98                             if ($inverseAssoc['type'] & ClassMetadata::TO_ONE) {
       
    99                                 $this->_hints['fetched'][$className][$inverseAssoc['fieldName']] = true;
       
   100                                 if ($class->subClasses) {
       
   101                                     foreach ($class->subClasses as $targetSubclassName) {
       
   102                                         $this->_hints['fetched'][$targetSubclassName][$inverseAssoc['fieldName']] = true;
       
   103                                     }
       
   104                                 }
       
   105                             }
       
   106                         }
       
   107                     }
       
   108                 }
       
   109             }
       
   110         }
       
   111     }
       
   112 
       
   113     /**
       
   114      * {@inheritdoc}
       
   115      */
       
   116     protected function _cleanup()
       
   117     {
       
   118         $eagerLoad = (isset($this->_hints['deferEagerLoad'])) && $this->_hints['deferEagerLoad'] == true;
       
   119         
       
   120         parent::_cleanup();
       
   121         $this->_identifierMap =
       
   122         $this->_initializedCollections =
       
   123         $this->_existingCollections =
       
   124         $this->_resultPointers = array();
       
   125         
       
   126         if ($eagerLoad) {
       
   127             $this->_em->getUnitOfWork()->triggerEagerLoads();
       
   128         }
       
   129     }
       
   130 
       
   131     /**
       
   132      * {@inheritdoc}
       
   133      */
       
   134     protected function _hydrateAll()
       
   135     {
       
   136         $result = array();
       
   137         $cache = array();
       
   138 
       
   139         while ($row = $this->_stmt->fetch(PDO::FETCH_ASSOC)) {
       
   140             $this->_hydrateRow($row, $cache, $result);
       
   141         }
       
   142 
       
   143         // Take snapshots from all newly initialized collections
       
   144         foreach ($this->_initializedCollections as $coll) {
       
   145             $coll->takeSnapshot();
       
   146         }
       
   147 
       
   148         return $result;
       
   149     }
       
   150 
       
   151     /**
       
   152      * Initializes a related collection.
       
   153      *
       
   154      * @param object $entity The entity to which the collection belongs.
       
   155      * @param string $name The name of the field on the entity that holds the collection.
       
   156      */
       
   157     private function _initRelatedCollection($entity, $class, $fieldName)
       
   158     {
       
   159         $oid = spl_object_hash($entity);
       
   160         $relation = $class->associationMappings[$fieldName];
       
   161 
       
   162         $value = $class->reflFields[$fieldName]->getValue($entity);
       
   163         if ($value === null) {
       
   164             $value = new ArrayCollection;
       
   165         }
       
   166 
       
   167         if ( ! $value instanceof PersistentCollection) {
       
   168             $value = new PersistentCollection(
       
   169                 $this->_em,
       
   170                 $this->_ce[$relation['targetEntity']],
       
   171                 $value
       
   172             );
       
   173             $value->setOwner($entity, $relation);
       
   174             $class->reflFields[$fieldName]->setValue($entity, $value);
       
   175             $this->_uow->setOriginalEntityProperty($oid, $fieldName, $value);
       
   176             $this->_initializedCollections[$oid . $fieldName] = $value;
       
   177         } else if (isset($this->_hints[Query::HINT_REFRESH]) ||
       
   178                 isset($this->_hints['fetched'][$class->name][$fieldName]) &&
       
   179                 ! $value->isInitialized()) {
       
   180             // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED!
       
   181             $value->setDirty(false);
       
   182             $value->setInitialized(true);
       
   183             $value->unwrap()->clear();
       
   184             $this->_initializedCollections[$oid . $fieldName] = $value;
       
   185         } else {
       
   186             // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN!
       
   187             $this->_existingCollections[$oid . $fieldName] = $value;
       
   188         }
       
   189 
       
   190         return $value;
       
   191     }
       
   192 
       
   193     /**
       
   194      * Gets an entity instance.
       
   195      * 
       
   196      * @param $data The instance data.
       
   197      * @param $dqlAlias The DQL alias of the entity's class.
       
   198      * @return object The entity.
       
   199      */
       
   200     private function _getEntity(array $data, $dqlAlias)
       
   201     {
       
   202         $className = $this->_rsm->aliasMap[$dqlAlias];
       
   203         if (isset($this->_rsm->discriminatorColumns[$dqlAlias])) {
       
   204             $discrColumn = $this->_rsm->metaMappings[$this->_rsm->discriminatorColumns[$dqlAlias]];
       
   205             $className = $this->_ce[$className]->discriminatorMap[$data[$discrColumn]];
       
   206             unset($data[$discrColumn]);
       
   207         }
       
   208         
       
   209         if (isset($this->_hints[Query::HINT_REFRESH_ENTITY]) && isset($this->_rootAliases[$dqlAlias])) {
       
   210             $class = $this->_ce[$className];
       
   211             $this->registerManaged($class, $this->_hints[Query::HINT_REFRESH_ENTITY], $data);
       
   212         }
       
   213         
       
   214         return $this->_uow->createEntity($className, $data, $this->_hints);
       
   215     }
       
   216 
       
   217     private function _getEntityFromIdentityMap($className, array $data)
       
   218     {
       
   219         // TODO: Abstract this code and UnitOfWork::createEntity() equivalent?
       
   220         $class = $this->_ce[$className];
       
   221         /* @var $class ClassMetadata */
       
   222         if ($class->isIdentifierComposite) {
       
   223             $idHash = '';
       
   224             foreach ($class->identifier as $fieldName) {
       
   225                 if (isset($class->associationMappings[$fieldName])) {
       
   226                     $idHash .= $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']] . ' ';
       
   227                 } else {
       
   228                     $idHash .= $data[$fieldName] . ' ';
       
   229                 }
       
   230             }
       
   231             return $this->_uow->tryGetByIdHash(rtrim($idHash), $class->rootEntityName);
       
   232         } else if (isset($class->associationMappings[$class->identifier[0]])) {
       
   233             return $this->_uow->tryGetByIdHash($data[$class->associationMappings[$class->identifier[0]]['joinColumns'][0]['name']], $class->rootEntityName);
       
   234         } else {
       
   235             return $this->_uow->tryGetByIdHash($data[$class->identifier[0]], $class->rootEntityName);
       
   236         }
       
   237     }
       
   238 
       
   239     /**
       
   240      * Gets a ClassMetadata instance from the local cache.
       
   241      * If the instance is not yet in the local cache, it is loaded into the
       
   242      * local cache.
       
   243      * 
       
   244      * @param string $className The name of the class.
       
   245      * @return ClassMetadata
       
   246      */
       
   247     private function _getClassMetadata($className)
       
   248     {
       
   249         if ( ! isset($this->_ce[$className])) {
       
   250             $this->_ce[$className] = $this->_em->getClassMetadata($className);
       
   251         }
       
   252         return $this->_ce[$className];
       
   253     }
       
   254 
       
   255     /**
       
   256      * Hydrates a single row in an SQL result set.
       
   257      * 
       
   258      * @internal
       
   259      * First, the data of the row is split into chunks where each chunk contains data
       
   260      * that belongs to a particular component/class. Afterwards, all these chunks
       
   261      * are processed, one after the other. For each chunk of class data only one of the
       
   262      * following code paths is executed:
       
   263      * 
       
   264      * Path A: The data chunk belongs to a joined/associated object and the association
       
   265      *         is collection-valued.
       
   266      * Path B: The data chunk belongs to a joined/associated object and the association
       
   267      *         is single-valued.
       
   268      * Path C: The data chunk belongs to a root result element/object that appears in the topmost
       
   269      *         level of the hydrated result. A typical example are the objects of the type
       
   270      *         specified by the FROM clause in a DQL query. 
       
   271      * 
       
   272      * @param array $data The data of the row to process.
       
   273      * @param array $cache The cache to use.
       
   274      * @param array $result The result array to fill.
       
   275      */
       
   276     protected function _hydrateRow(array $data, array &$cache, array &$result)
       
   277     {
       
   278         // Initialize
       
   279         $id = $this->_idTemplate; // initialize the id-memory
       
   280         $nonemptyComponents = array();
       
   281         // Split the row data into chunks of class data.
       
   282         $rowData = $this->_gatherRowData($data, $cache, $id, $nonemptyComponents);
       
   283 
       
   284         // Extract scalar values. They're appended at the end.
       
   285         if (isset($rowData['scalars'])) {
       
   286             $scalars = $rowData['scalars'];
       
   287             unset($rowData['scalars']);
       
   288             if (empty($rowData)) {
       
   289                 ++$this->_resultCounter;
       
   290             }
       
   291         }
       
   292 
       
   293         // Hydrate the data chunks
       
   294         foreach ($rowData as $dqlAlias => $data) {
       
   295             $entityName = $this->_rsm->aliasMap[$dqlAlias];
       
   296 
       
   297             if (isset($this->_rsm->parentAliasMap[$dqlAlias])) {
       
   298                 // It's a joined result
       
   299 
       
   300                 $parentAlias = $this->_rsm->parentAliasMap[$dqlAlias];
       
   301                 // we need the $path to save into the identifier map which entities were already
       
   302                 // seen for this parent-child relationship
       
   303                 $path = $parentAlias . '.' . $dqlAlias;
       
   304 
       
   305                 // Get a reference to the parent object to which the joined element belongs.
       
   306                 if ($this->_rsm->isMixed && isset($this->_rootAliases[$parentAlias])) {
       
   307                     $first = reset($this->_resultPointers);
       
   308                     $parentObject = $this->_resultPointers[$parentAlias][key($first)];
       
   309                 } else if (isset($this->_resultPointers[$parentAlias])) {
       
   310                     $parentObject = $this->_resultPointers[$parentAlias];
       
   311                 } else {
       
   312                     // Parent object of relation not found, so skip it.
       
   313                     continue;
       
   314                 }
       
   315 
       
   316                 $parentClass = $this->_ce[$this->_rsm->aliasMap[$parentAlias]];
       
   317                 $oid = spl_object_hash($parentObject);
       
   318                 $relationField = $this->_rsm->relationMap[$dqlAlias];
       
   319                 $relation = $parentClass->associationMappings[$relationField];
       
   320                 $reflField = $parentClass->reflFields[$relationField];
       
   321 
       
   322                 // Check the type of the relation (many or single-valued)
       
   323                 if ( ! ($relation['type'] & ClassMetadata::TO_ONE)) {
       
   324                     // PATH A: Collection-valued association
       
   325                     if (isset($nonemptyComponents[$dqlAlias])) {
       
   326                         $collKey = $oid . $relationField;
       
   327                         if (isset($this->_initializedCollections[$collKey])) {
       
   328                             $reflFieldValue = $this->_initializedCollections[$collKey];
       
   329                         } else if ( ! isset($this->_existingCollections[$collKey])) {
       
   330                             $reflFieldValue = $this->_initRelatedCollection($parentObject, $parentClass, $relationField);
       
   331                         }
       
   332 
       
   333                         $indexExists = isset($this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]);
       
   334                         $index = $indexExists ? $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false;
       
   335                         $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false;
       
   336 
       
   337                         if ( ! $indexExists || ! $indexIsValid) {
       
   338                             if (isset($this->_existingCollections[$collKey])) {
       
   339                                 // Collection exists, only look for the element in the identity map.
       
   340                                 if ($element = $this->_getEntityFromIdentityMap($entityName, $data)) {
       
   341                                     $this->_resultPointers[$dqlAlias] = $element;
       
   342                                 } else {
       
   343                                     unset($this->_resultPointers[$dqlAlias]);
       
   344                                 }
       
   345                             } else {
       
   346                                 $element = $this->_getEntity($data, $dqlAlias);
       
   347 
       
   348                                 if (isset($this->_rsm->indexByMap[$dqlAlias])) {
       
   349                                     $field = $this->_rsm->indexByMap[$dqlAlias];
       
   350                                     $indexValue = $this->_ce[$entityName]->reflFields[$field]->getValue($element);
       
   351                                     $reflFieldValue->hydrateSet($indexValue, $element);
       
   352                                     $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue;
       
   353                                 } else {
       
   354                                     $reflFieldValue->hydrateAdd($element);
       
   355                                     $reflFieldValue->last();
       
   356                                     $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key();
       
   357                                 }
       
   358                                 // Update result pointer
       
   359                                 $this->_resultPointers[$dqlAlias] = $element;
       
   360                             }
       
   361                         } else {
       
   362                             // Update result pointer
       
   363                             $this->_resultPointers[$dqlAlias] = $reflFieldValue[$index];
       
   364                         }
       
   365                     } else if ( ! $reflField->getValue($parentObject)) {
       
   366                         $coll = new PersistentCollection($this->_em, $this->_ce[$entityName], new ArrayCollection);
       
   367                         $coll->setOwner($parentObject, $relation);
       
   368                         $reflField->setValue($parentObject, $coll);
       
   369                         $this->_uow->setOriginalEntityProperty($oid, $relationField, $coll);
       
   370                     }
       
   371                 } else {
       
   372                     // PATH B: Single-valued association
       
   373                     $reflFieldValue = $reflField->getValue($parentObject);
       
   374                     if ( ! $reflFieldValue || isset($this->_hints[Query::HINT_REFRESH])) {
       
   375                         if (isset($nonemptyComponents[$dqlAlias])) {
       
   376                             $element = $this->_getEntity($data, $dqlAlias);
       
   377                             $reflField->setValue($parentObject, $element);
       
   378                             $this->_uow->setOriginalEntityProperty($oid, $relationField, $element);
       
   379                             $targetClass = $this->_ce[$relation['targetEntity']];
       
   380                             if ($relation['isOwningSide']) {
       
   381                                 //TODO: Just check hints['fetched'] here?
       
   382                                 // If there is an inverse mapping on the target class its bidirectional
       
   383                                 if ($relation['inversedBy']) {
       
   384                                     $inverseAssoc = $targetClass->associationMappings[$relation['inversedBy']];
       
   385                                     if ($inverseAssoc['type'] & ClassMetadata::TO_ONE) {
       
   386                                         $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($element, $parentObject);
       
   387                                         $this->_uow->setOriginalEntityProperty(spl_object_hash($element), $inverseAssoc['fieldName'], $parentObject);
       
   388                                     }
       
   389                                 } else if ($parentClass === $targetClass && $relation['mappedBy']) {
       
   390                                     // Special case: bi-directional self-referencing one-one on the same class
       
   391                                     $targetClass->reflFields[$relationField]->setValue($element, $parentObject);
       
   392                                 }
       
   393                             } else {
       
   394                                 // For sure bidirectional, as there is no inverse side in unidirectional mappings
       
   395                                 $targetClass->reflFields[$relation['mappedBy']]->setValue($element, $parentObject);
       
   396                                 $this->_uow->setOriginalEntityProperty(spl_object_hash($element), $relation['mappedBy'], $parentObject);
       
   397                             }
       
   398                             // Update result pointer
       
   399                             $this->_resultPointers[$dqlAlias] = $element;
       
   400                         }
       
   401                         // else leave $reflFieldValue null for single-valued associations
       
   402                     } else {
       
   403                         // Update result pointer
       
   404                         $this->_resultPointers[$dqlAlias] = $reflFieldValue;
       
   405                     }
       
   406                 }
       
   407             } else {
       
   408                 // PATH C: Its a root result element
       
   409                 $this->_rootAliases[$dqlAlias] = true; // Mark as root alias
       
   410 
       
   411                 if ( ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) {
       
   412                     $element = $this->_getEntity($rowData[$dqlAlias], $dqlAlias);
       
   413                     if (isset($this->_rsm->indexByMap[$dqlAlias])) {
       
   414                         $field = $this->_rsm->indexByMap[$dqlAlias];
       
   415                         $key = $this->_ce[$entityName]->reflFields[$field]->getValue($element);
       
   416                         if ($this->_rsm->isMixed) {
       
   417                             $element = array($key => $element);
       
   418                             $result[] = $element;
       
   419                             $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $this->_resultCounter;
       
   420                             ++$this->_resultCounter;
       
   421                         } else {
       
   422                             $result[$key] = $element;
       
   423                             $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $key;
       
   424                         }
       
   425 
       
   426                         if (isset($this->_hints['collection'])) {
       
   427                             $this->_hints['collection']->hydrateSet($key, $element);
       
   428                         }
       
   429                     } else {
       
   430                         if ($this->_rsm->isMixed) {
       
   431                             $element = array(0 => $element);
       
   432                         }
       
   433                         $result[] = $element;
       
   434                         $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $this->_resultCounter;
       
   435                         ++$this->_resultCounter;
       
   436 
       
   437                         if (isset($this->_hints['collection'])) {
       
   438                             $this->_hints['collection']->hydrateAdd($element);
       
   439                         }
       
   440                     }
       
   441 
       
   442                     // Update result pointer
       
   443                     $this->_resultPointers[$dqlAlias] = $element;
       
   444 
       
   445                 } else {
       
   446                     // Update result pointer
       
   447                     $index = $this->_identifierMap[$dqlAlias][$id[$dqlAlias]];
       
   448                     $this->_resultPointers[$dqlAlias] = $result[$index];
       
   449                     /*if ($this->_rsm->isMixed) {
       
   450                         $result[] = $result[$index];
       
   451                         ++$this->_resultCounter;
       
   452                     }*/
       
   453                 }
       
   454             }
       
   455         }
       
   456 
       
   457         // Append scalar values to mixed result sets
       
   458         if (isset($scalars)) {
       
   459             foreach ($scalars as $name => $value) {
       
   460                 $result[$this->_resultCounter - 1][$name] = $value;
       
   461             }
       
   462         }
       
   463     }
       
   464 }