/** * Set up the PersistentCollection used for collection initialization tests. */ public function setUpPersistentCollection() { $classMetaData = $this->_emMock->getClassMetadata('Doctrine\\Tests\\Models\\ECommerce\\ECommerceCart'); $this->collection = new PersistentCollection($this->_emMock, $classMetaData, new ArrayCollection()); $this->collection->setInitialized(false); $this->collection->setOwner(new ECommerceCart(), $classMetaData->getAssociationMapping('products')); }
public function testShouldNotScheduleDeletionOnClonedInstances() { $class = $this->_em->getClassMetadata('Doctrine\\Tests\\Models\\ECommerce\\ECommerceProduct'); $product = new ECommerceProduct(); $category = new ECommerceCategory(); $collection = new PersistentCollection($this->_em, $class, new ArrayCollection(array($category))); $collection->setOwner($product, $class->associationMappings['categories']); $uow = $this->_em->getUnitOfWork(); $clonedCollection = clone $collection; $clonedCollection->clear(); $this->assertEquals(0, count($uow->getScheduledCollectionDeletions())); }
/** * Prepare a lazy loadable PersistentCollection * on the entity to get Products. * The entity must have a "products" property defined * * @param object $entity The entity related to the products * @param array $assoc Association properties * @param EntityManager $entityManager Entity manager */ protected function setProductPersistentCollection($entity, $assoc, EntityManager $entityManager) { $targetEntity = $this->productClass; $productsCollection = new PersistentCollection($entityManager, $targetEntity, new ArrayCollection()); $assoc['fieldName'] = 'products'; $assoc['targetEntity'] = $targetEntity; $assoc['type'] = ClassMetadata::MANY_TO_MANY; $assoc['inversedBy'] = ''; $assoc['isOwningSide'] = false; $assoc['sourceEntity'] = get_class($entity); $assoc['orphanRemoval'] = false; $productsCollection->setOwner($entity, $assoc); $productsCollection->setInitialized(false); $entityMetadata = $entityManager->getClassMetadata(get_class($entity)); $productsReflProp = $entityMetadata->reflClass->getProperty('products'); $productsReflProp->setAccessible(true); $productsReflProp->setValue($entity, $productsCollection); }
/** * Initializes a related collection. * * @param object $entity The entity to which the collection belongs. * @param ClassMetadata $class * @param string $fieldName The name of the field on the entity that holds the collection. * @param string $parentDqlAlias Alias of the parent fetch joining this collection. * * @return \Doctrine\ORM\PersistentCollection */ private function initRelatedCollection($entity, $class, $fieldName, $parentDqlAlias) { $oid = spl_object_hash($entity); $relation = $class->associationMappings[$fieldName]; $value = $class->reflFields[$fieldName]->getValue($entity); if ($value === null || is_array($value)) { $value = new ArrayCollection((array) $value); } if (!$value instanceof PersistentCollection) { $value = new PersistentCollection($this->_em, $this->_metadataCache[$relation['targetEntity']], $value); $value->setOwner($entity, $relation); $class->reflFields[$fieldName]->setValue($entity, $value); $this->_uow->setOriginalEntityProperty($oid, $fieldName, $value); $this->initializedCollections[$oid . $fieldName] = $value; } else { if (isset($this->_hints[Query::HINT_REFRESH]) || isset($this->_hints['fetched'][$parentDqlAlias][$fieldName]) && !$value->isInitialized()) { // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED! $value->setDirty(false); $value->setInitialized(true); $value->unwrap()->clear(); $this->initializedCollections[$oid . $fieldName] = $value; } else { // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN! $this->existingCollections[$oid . $fieldName] = $value; } } return $value; }
/** * @param object $owner * @param array $mapping * @param array $items * * @return PersistentCollection */ protected function getPersistentCollection($owner, array $mapping, array $items = []) { $metadata = $this->getMockBuilder('Doctrine\\ORM\\Mapping\\ClassMetadata')->disableOriginalConstructor()->getMock(); $coll = new PersistentCollection($this->em, $metadata, new ArrayCollection($items)); $mapping['inversedBy'] = 'test'; $coll->setOwner($owner, $mapping); return $coll; }
/** * INTERNAL: * Creates an entity. Used for reconstitution of persistent entities. * * @ignore * * @param string $className The name of the entity class. * @param array $data The data for the entity. * @param array $hints Any hints to account for during reconstitution/lookup of the entity. * * @return object The managed entity instance. * * @internal Highly performance-sensitive method. * * @todo Rename: getOrCreateEntity */ public function createEntity($className, array $data, &$hints = array()) { $class = $this->em->getClassMetadata($className); //$isReadOnly = isset($hints[Query::HINT_READ_ONLY]); if ($class->isIdentifierComposite) { $id = array(); foreach ($class->identifier as $fieldName) { $id[$fieldName] = isset($class->associationMappings[$fieldName]) ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']] : $data[$fieldName]; } } else { $id = isset($class->associationMappings[$class->identifier[0]]) ? $data[$class->associationMappings[$class->identifier[0]]['joinColumns'][0]['name']] : $data[$class->identifier[0]]; $id = array($class->identifier[0] => $id); } $idHash = implode(' ', $id); if (isset($this->identityMap[$class->rootEntityName][$idHash])) { $entity = $this->identityMap[$class->rootEntityName][$idHash]; $oid = spl_object_hash($entity); if (isset($hints[Query::HINT_REFRESH]) && isset($hints[Query::HINT_REFRESH_ENTITY]) && ($unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY]) !== $entity && $unmanagedProxy instanceof Proxy && $this->isIdentifierEquals($unmanagedProxy, $entity)) { // DDC-1238 - we have a managed instance, but it isn't the provided one. // Therefore we clear its identifier. Also, we must re-fetch metadata since the // refreshed object may be anything foreach ($class->identifier as $fieldName) { $class->reflFields[$fieldName]->setValue($unmanagedProxy, null); } return $unmanagedProxy; } if ($entity instanceof Proxy && !$entity->__isInitialized()) { $entity->__setInitialized(true); $overrideLocalValues = true; if ($entity instanceof NotifyPropertyChanged) { $entity->addPropertyChangedListener($this); } } else { $overrideLocalValues = isset($hints[Query::HINT_REFRESH]); // If only a specific entity is set to refresh, check that it's the one if (isset($hints[Query::HINT_REFRESH_ENTITY])) { $overrideLocalValues = $hints[Query::HINT_REFRESH_ENTITY] === $entity; } } if ($overrideLocalValues) { // inject ObjectManager upon refresh. if ($entity instanceof ObjectManagerAware) { $entity->injectObjectManager($this->em, $class); } $this->originalEntityData[$oid] = $data; } } else { $entity = $this->newInstance($class); $oid = spl_object_hash($entity); $this->entityIdentifiers[$oid] = $id; $this->entityStates[$oid] = self::STATE_MANAGED; $this->originalEntityData[$oid] = $data; $this->identityMap[$class->rootEntityName][$idHash] = $entity; if ($entity instanceof NotifyPropertyChanged) { $entity->addPropertyChangedListener($this); } $overrideLocalValues = true; } if (!$overrideLocalValues) { return $entity; } foreach ($data as $field => $value) { if (isset($class->fieldMappings[$field])) { $class->reflFields[$field]->setValue($entity, $value); } } // Loading the entity right here, if its in the eager loading map get rid of it there. unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]); if (isset($this->eagerLoadingEntities[$class->rootEntityName]) && !$this->eagerLoadingEntities[$class->rootEntityName]) { unset($this->eagerLoadingEntities[$class->rootEntityName]); } // Properly initialize any unfetched associations, if partial objects are not allowed. if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) { return $entity; } foreach ($class->associationMappings as $field => $assoc) { // Check if the association is not among the fetch-joined associations already. if (isset($hints['fetchAlias']) && isset($hints['fetched'][$hints['fetchAlias']][$field])) { continue; } $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); switch (true) { case $assoc['type'] & ClassMetadata::TO_ONE: if (!$assoc['isOwningSide']) { // use the given entity association if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) { $this->originalEntityData[$oid][$field] = $data[$field]; $class->reflFields[$field]->setValue($entity, $data[$field]); $targetClass->reflFields[$assoc['mappedBy']]->setValue($data[$field], $entity); continue 2; } // Inverse side of x-to-one can never be lazy $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity)); continue 2; } // use the entity association if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) { $class->reflFields[$field]->setValue($entity, $data[$field]); $this->originalEntityData[$oid][$field] = $data[$field]; continue; } $associatedId = array(); // TODO: Is this even computed right in all cases of composite keys? foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { $joinColumnValue = isset($data[$srcColumn]) ? $data[$srcColumn] : null; if ($joinColumnValue !== null) { if ($targetClass->containsForeignIdentifier) { $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue; } else { $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue; } } } if (!$associatedId) { // Foreign key is NULL $class->reflFields[$field]->setValue($entity, null); $this->originalEntityData[$oid][$field] = null; continue; } if (!isset($hints['fetchMode'][$class->name][$field])) { $hints['fetchMode'][$class->name][$field] = $assoc['fetch']; } // Foreign key is set // Check identity map first // FIXME: Can break easily with composite keys if join column values are in // wrong order. The correct order is the one in ClassMetadata#identifier. $relatedIdHash = implode(' ', $associatedId); switch (true) { case isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash]): $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash]; // If this is an uninitialized proxy, we are deferring eager loads, // this association is marked as eager fetch, and its an uninitialized proxy (wtf!) // then we can append this entity for eager loading! if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER && isset($hints[self::HINT_DEFEREAGERLOAD]) && !$targetClass->isIdentifierComposite && $newValue instanceof Proxy && $newValue->__isInitialized__ === false) { $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); } break; case $targetClass->subClasses: // If it might be a subtype, it can not be lazy. There isn't even // a way to solve this with deferred eager loading, which means putting // an entity with subclasses at a *-to-one location is really bad! (performance-wise) $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId); break; default: switch (true) { // We are negating the condition here. Other cases will assume it is valid! case $hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER: $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); break; // Deferred eager load only works for single identifier classes // Deferred eager load only works for single identifier classes case isset($hints[self::HINT_DEFEREAGERLOAD]) && !$targetClass->isIdentifierComposite: // TODO: Is there a faster approach? $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); break; default: // TODO: This is very imperformant, ignore it? $newValue = $this->em->find($assoc['targetEntity'], $associatedId); break; } // PERF: Inlined & optimized code from UnitOfWork#registerManaged() $newValueOid = spl_object_hash($newValue); $this->entityIdentifiers[$newValueOid] = $associatedId; $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue; if ($newValue instanceof NotifyPropertyChanged && (!$newValue instanceof Proxy || $newValue->__isInitialized())) { $newValue->addPropertyChangedListener($this); } $this->entityStates[$newValueOid] = self::STATE_MANAGED; // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also! break; } $this->originalEntityData[$oid][$field] = $newValue; $class->reflFields[$field]->setValue($entity, $newValue); if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) { $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']]; $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity); } break; default: // Ignore if its a cached collection if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) { break; } // use the given collection if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) { $data[$field]->setOwner($entity, $assoc); $class->reflFields[$field]->setValue($entity, $data[$field]); $this->originalEntityData[$oid][$field] = $data[$field]; break; } // Inject collection $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection()); $pColl->setOwner($entity, $assoc); $pColl->setInitialized(false); $reflField = $class->reflFields[$field]; $reflField->setValue($entity, $pColl); if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { $this->loadCollection($pColl); $pColl->takeSnapshot(); } $this->originalEntityData[$oid][$field] = $pColl; break; } } if ($overrideLocalValues) { $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postLoad); if ($invoke !== ListenersInvoker::INVOKE_NONE) { $this->listenersInvoker->invoke($class, Events::postLoad, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); } } return $entity; }
/** * INTERNAL: * Creates an entity. Used for reconstitution of persistent entities. * * @ignore * @param string $className The name of the entity class. * @param array $data The data for the entity. * @param array $hints Any hints to account for during reconstitution/lookup of the entity. * @return object The managed entity instance. * @internal Highly performance-sensitive method. * * @todo Rename: getOrCreateEntity */ public function createEntity($className, array $data, &$hints = array()) { $class = $this->em->getClassMetadata($className); //$isReadOnly = isset($hints[Query::HINT_READ_ONLY]); if ($class->isIdentifierComposite) { $id = array(); foreach ($class->identifier as $fieldName) { $id[$fieldName] = $data[$fieldName]; } $idHash = implode(' ', $id); } else { $idHash = $data[$class->identifier[0]]; $id = array($class->identifier[0] => $idHash); } if (isset($this->identityMap[$class->rootEntityName][$idHash])) { $entity = $this->identityMap[$class->rootEntityName][$idHash]; $oid = spl_object_hash($entity); if ($entity instanceof Proxy && !$entity->__isInitialized__) { $entity->__isInitialized__ = true; $overrideLocalValues = true; $this->originalEntityData[$oid] = $data; if ($entity instanceof NotifyPropertyChanged) { $entity->addPropertyChangedListener($this); } } else { $overrideLocalValues = isset($hints[Query::HINT_REFRESH]); } } else { $entity = $class->newInstance(); $oid = spl_object_hash($entity); $this->entityIdentifiers[$oid] = $id; $this->entityStates[$oid] = self::STATE_MANAGED; $this->originalEntityData[$oid] = $data; $this->identityMap[$class->rootEntityName][$idHash] = $entity; if ($entity instanceof NotifyPropertyChanged) { $entity->addPropertyChangedListener($this); } $overrideLocalValues = true; } if ($overrideLocalValues) { foreach ($data as $field => $value) { if (isset($class->fieldMappings[$field])) { $class->reflFields[$field]->setValue($entity, $value); } } // Properly initialize any unfetched associations, if partial objects are not allowed. if (!isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) { foreach ($class->associationMappings as $field => $assoc) { // Check if the association is not among the fetch-joined associations already. if (isset($hints['fetched'][$className][$field])) { continue; } $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); if ($assoc['type'] & ClassMetadata::TO_ONE) { if ($assoc['isOwningSide']) { $associatedId = array(); foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { $joinColumnValue = isset($data[$srcColumn]) ? $data[$srcColumn] : null; if ($joinColumnValue !== null) { $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue; } } if (!$associatedId) { // Foreign key is NULL $class->reflFields[$field]->setValue($entity, null); $this->originalEntityData[$oid][$field] = null; } else { // Foreign key is set // Check identity map first // FIXME: Can break easily with composite keys if join column values are in // wrong order. The correct order is the one in ClassMetadata#identifier. $relatedIdHash = implode(' ', $associatedId); if (isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash])) { $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash]; } else { if ($targetClass->subClasses) { // If it might be a subtype, it can not be lazy $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, null, $associatedId); } else { if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { // TODO: Maybe it could be optimized to do an eager fetch with a JOIN inside // the persister instead of this rather unperformant approach. $newValue = $this->em->find($assoc['targetEntity'], $associatedId); } else { $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); } // PERF: Inlined & optimized code from UnitOfWork#registerManaged() $newValueOid = spl_object_hash($newValue); $this->entityIdentifiers[$newValueOid] = $associatedId; $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue; $this->entityStates[$newValueOid] = self::STATE_MANAGED; // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also! } } $this->originalEntityData[$oid][$field] = $newValue; $class->reflFields[$field]->setValue($entity, $newValue); } } else { // Inverse side of x-to-one can never be lazy $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, null)); } } else { // Inject collection $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection()); $pColl->setOwner($entity, $assoc); $reflField = $class->reflFields[$field]; $reflField->setValue($entity, $pColl); if ($assoc['fetch'] == ClassMetadata::FETCH_LAZY) { $pColl->setInitialized(false); } else { $this->loadCollection($pColl); $pColl->takeSnapshot(); } $this->originalEntityData[$oid][$field] = $pColl; } } } } //TODO: These should be invoked later, after hydration, because associations may not yet be loaded here. if (isset($class->lifecycleCallbacks[Events::postLoad])) { $class->invokeLifecycleCallbacks(Events::postLoad, $entity); } if ($this->evm->hasListeners(Events::postLoad)) { $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity, $this->em)); } return $entity; }
/** * @param object $entity * @param object $managedCopy * * @throws ORMException * @throws OptimisticLockException * @throws TransactionRequiredException */ private function mergeEntityStateIntoManagedCopy($entity, $managedCopy) { $class = $this->em->getClassMetadata(get_class($entity)); foreach ($this->reflectionPropertiesGetter->getProperties($class->name) as $prop) { $name = $prop->name; $prop->setAccessible(true); if (!isset($class->associationMappings[$name])) { if (!$class->isIdentifier($name)) { $prop->setValue($managedCopy, $prop->getValue($entity)); } } else { $assoc2 = $class->associationMappings[$name]; if ($assoc2['type'] & ClassMetadata::TO_ONE) { $other = $prop->getValue($entity); if ($other === null) { $prop->setValue($managedCopy, null); } else { if ($other instanceof Proxy && !$other->__isInitialized()) { // do not merge fields marked lazy that have not been fetched. return; } if (!$assoc2['isCascadeMerge']) { if ($this->getEntityState($other) === self::STATE_DETACHED) { $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']); $relatedId = $targetClass->getIdentifierValues($other); if ($targetClass->subClasses) { $other = $this->em->find($targetClass->name, $relatedId); } else { $other = $this->em->getProxyFactory()->getProxy($assoc2['targetEntity'], $relatedId); $this->registerManaged($other, $relatedId, array()); } } $prop->setValue($managedCopy, $other); } } } else { $mergeCol = $prop->getValue($entity); if ($mergeCol instanceof PersistentCollection && !$mergeCol->isInitialized()) { // do not merge fields marked lazy that have not been fetched. // keep the lazy persistent collection of the managed copy. return; } $managedCol = $prop->getValue($managedCopy); if (!$managedCol) { $managedCol = new PersistentCollection($this->em, $this->em->getClassMetadata($assoc2['targetEntity']), new ArrayCollection()); $managedCol->setOwner($managedCopy, $assoc2); $prop->setValue($managedCopy, $managedCol); $this->originalEntityData[spl_object_hash($entity)][$name] = $managedCol; } if ($assoc2['isCascadeMerge']) { $managedCol->initialize(); // clear and set dirty a managed collection if its not also the same collection to merge from. if (!$managedCol->isEmpty() && $managedCol !== $mergeCol) { $managedCol->unwrap()->clear(); $managedCol->setDirty(true); if ($assoc2['isOwningSide'] && $assoc2['type'] == ClassMetadata::MANY_TO_MANY && $class->isChangeTrackingNotify()) { $this->scheduleForDirtyCheck($managedCopy); } } } } } if ($class->isChangeTrackingNotify()) { // Just treat all properties as changed, there is no other choice. $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy)); } } }
/** * INTERNAL: * Creates an entity. Used for reconstitution of persistent entities. * * @ignore * @param string $className The name of the entity class. * @param array $data The data for the entity. * @param array $hints Any hints to account for during reconstitution/lookup of the entity. * @return object The managed entity instance. * @internal Highly performance-sensitive method. * * @todo Rename: getOrCreateEntity */ public function createEntity($className, array $data, &$hints = array()) { $class = $this->em->getClassMetadata($className); //$isReadOnly = isset($hints[Query::HINT_READ_ONLY]); if ($class->isIdentifierComposite) { $id = array(); foreach ($class->identifier as $fieldName) { if (isset($class->associationMappings[$fieldName])) { $id[$fieldName] = $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]; } else { $id[$fieldName] = $data[$fieldName]; } } $idHash = implode(' ', $id); } else { if (isset($class->associationMappings[$class->identifier[0]])) { $idHash = $data[$class->associationMappings[$class->identifier[0]]['joinColumns'][0]['name']]; } else { $idHash = $data[$class->identifier[0]]; } $id = array($class->identifier[0] => $idHash); } if (isset($this->identityMap[$class->rootEntityName][$idHash])) { $entity = $this->identityMap[$class->rootEntityName][$idHash]; $oid = spl_object_hash($entity); if ($entity instanceof Proxy && !$entity->__isInitialized__) { $entity->__isInitialized__ = true; $overrideLocalValues = true; if ($entity instanceof NotifyPropertyChanged) { $entity->addPropertyChangedListener($this); } } else { $overrideLocalValues = isset($hints[Query::HINT_REFRESH]); // If only a specific entity is set to refresh, check that it's the one if (isset($hints[Query::HINT_REFRESH_ENTITY])) { $overrideLocalValues = $hints[Query::HINT_REFRESH_ENTITY] === $entity; } } if ($overrideLocalValues) { $this->originalEntityData[$oid] = $data; } } else { $entity = $class->newInstance(); $oid = spl_object_hash($entity); $this->entityIdentifiers[$oid] = $id; $this->entityStates[$oid] = self::STATE_MANAGED; $this->originalEntityData[$oid] = $data; $this->identityMap[$class->rootEntityName][$idHash] = $entity; if ($entity instanceof NotifyPropertyChanged) { $entity->addPropertyChangedListener($this); } $overrideLocalValues = true; } if ($overrideLocalValues) { foreach ($data as $field => $value) { if (isset($class->fieldMappings[$field])) { $class->reflFields[$field]->setValue($entity, $value); } } // Loading the entity right here, if its in the eager loading map get rid of it there. unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]); // Properly initialize any unfetched associations, if partial objects are not allowed. if (!isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) { foreach ($class->associationMappings as $field => $assoc) { // Check if the association is not among the fetch-joined associations already. if (isset($hints['fetchAlias']) && isset($hints['fetched'][$hints['fetchAlias']][$field])) { continue; } $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); if ($assoc['type'] & ClassMetadata::TO_ONE) { if ($assoc['isOwningSide']) { $associatedId = array(); // TODO: Is this even computed right in all cases of composite keys? foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { $joinColumnValue = isset($data[$srcColumn]) ? $data[$srcColumn] : null; if ($joinColumnValue !== null) { if ($targetClass->containsForeignIdentifier) { $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue; } else { $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue; } } } if (!$associatedId) { // Foreign key is NULL $class->reflFields[$field]->setValue($entity, null); $this->originalEntityData[$oid][$field] = null; } else { if (!isset($hints['fetchMode'][$class->name][$field])) { $hints['fetchMode'][$class->name][$field] = $assoc['fetch']; } // Foreign key is set // Check identity map first // FIXME: Can break easily with composite keys if join column values are in // wrong order. The correct order is the one in ClassMetadata#identifier. $relatedIdHash = implode(' ', $associatedId); if (isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash])) { $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash]; // if this is an uninitialized proxy, we are deferring eager loads, // this association is marked as eager fetch, and its an uninitialized proxy (wtf!) // then we cann append this entity for eager loading! if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER && isset($hints['deferEagerLoad']) && !$targetClass->isIdentifierComposite && $newValue instanceof Proxy && $newValue->__isInitialized__ === false) { $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); } } else { if ($targetClass->subClasses) { // If it might be a subtype, it can not be lazy. There isn't even // a way to solve this with deferred eager loading, which means putting // an entity with subclasses at a *-to-one location is really bad! (performance-wise) $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId); } else { // Deferred eager load only works for single identifier classes if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER) { if (isset($hints['deferEagerLoad']) && !$targetClass->isIdentifierComposite) { // TODO: Is there a faster approach? $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); } else { // TODO: This is very imperformant, ignore it? $newValue = $this->em->find($assoc['targetEntity'], $associatedId); } } else { $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); } // PERF: Inlined & optimized code from UnitOfWork#registerManaged() $newValueOid = spl_object_hash($newValue); $this->entityIdentifiers[$newValueOid] = $associatedId; $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue; $this->entityStates[$newValueOid] = self::STATE_MANAGED; // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also! } } $this->originalEntityData[$oid][$field] = $newValue; $class->reflFields[$field]->setValue($entity, $newValue); if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) { $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']]; $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity); } } } else { // Inverse side of x-to-one can never be lazy $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity)); } } else { // Inject collection $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection()); $pColl->setOwner($entity, $assoc); $reflField = $class->reflFields[$field]; $reflField->setValue($entity, $pColl); if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { $this->loadCollection($pColl); $pColl->takeSnapshot(); } else { $pColl->setInitialized(false); } $this->originalEntityData[$oid][$field] = $pColl; } } } } //TODO: These should be invoked later, after hydration, because associations may not yet be loaded here. if (isset($class->lifecycleCallbacks[Events::postLoad])) { $class->invokeLifecycleCallbacks(Events::postLoad, $entity); } if ($this->evm->hasListeners(Events::postLoad)) { $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity, $this->em)); } return $entity; }
/** * Initializes a related collection. * * @param object $entity The entity to which the collection belongs. * @param string $name The name of the field on the entity that holds the collection. */ private function _initRelatedCollection($entity, $class, $fieldName) { $oid = spl_object_hash($entity); $relation = $class->associationMappings[$fieldName]; $value = $class->reflFields[$fieldName]->getValue($entity); if ($value === null) { $value = new ArrayCollection(); } if (!$value instanceof PersistentCollection) { $value = new PersistentCollection($this->_em, $this->_ce[$relation->targetEntityName], $value); $value->setOwner($entity, $relation); $class->reflFields[$fieldName]->setValue($entity, $value); $this->_uow->setOriginalEntityProperty($oid, $fieldName, $value); $this->_initializedCollections[$oid . $fieldName] = $value; } else { if (isset($this->_hints[Query::HINT_REFRESH])) { // Is already PersistentCollection, but REFRESH $value->setDirty(false); $value->setInitialized(true); $value->unwrap()->clear(); $this->_initializedCollections[$oid . $fieldName] = $value; } else { // Is already PersistentCollection, and DONT REFRESH $this->_existingCollections[$oid . $fieldName] = $value; } } return $value; }
/** * Computes the changes done to a single entity. * * Modifies/populates the following properties: * * {@link _originalEntityData} * If the entity is NEW or MANAGED but not yet fully persisted (only has an id) * then it was not fetched from the database and therefore we have no original * entity data yet. All of the current entity data is stored as the original entity data. * * {@link _entityChangeSets} * The changes detected on all properties of the entity are stored there. * A change is a tuple array where the first entry is the old value and the second * entry is the new value of the property. Changesets are used by persisters * to INSERT/UPDATE the persistent entity state. * * {@link _entityUpdates} * If the entity is already fully MANAGED (has been fetched from the database before) * and any changes to its properties are detected, then a reference to the entity is stored * there to mark it for an update. * * {@link _collectionDeletions} * If a PersistentCollection has been de-referenced in a fully MANAGED entity, * then this collection is marked for deletion. * * @param ClassMetadata $class The class descriptor of the entity. * @param object $entity The entity for which to compute the changes. */ private function _computeEntityChanges($class, $entity) { $oid = spl_object_hash($entity); if (!$class->isInheritanceTypeNone()) { $class = $this->_em->getClassMetadata(get_class($entity)); } $actualData = array(); foreach ($class->reflFields as $name => $refProp) { if (!$class->isIdentifier($name) || !$class->isIdGeneratorIdentity()) { $actualData[$name] = $refProp->getValue($entity); } if ($class->isCollectionValuedAssociation($name) && $actualData[$name] !== null && !$actualData[$name] instanceof PersistentCollection) { //TODO: If $actualData[$name] is Collection then unwrap the array $assoc = $class->associationMappings[$name]; //echo PHP_EOL . "INJECTING PCOLL into $name" . PHP_EOL; // Inject PersistentCollection $coll = new PersistentCollection($this->_em, $this->_em->getClassMetadata($assoc->targetEntityName), $actualData[$name] ? $actualData[$name] : array()); $coll->setOwner($entity, $assoc); if (!$coll->isEmpty()) { $coll->setDirty(true); } $class->reflFields[$name]->setValue($entity, $coll); $actualData[$name] = $coll; } } if (!isset($this->_originalEntityData[$oid])) { // Entity is either NEW or MANAGED but not yet fully persisted // (only has an id). These result in an INSERT. $this->_originalEntityData[$oid] = $actualData; $this->_entityChangeSets[$oid] = array_map(function ($e) { return array(null, $e); }, $actualData); } else { // Entity is "fully" MANAGED: it was already fully persisted before // and we have a copy of the original data $originalData = $this->_originalEntityData[$oid]; $changeSet = array(); $entityIsDirty = false; foreach ($actualData as $propName => $actualValue) { $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null; if (is_object($orgValue) && $orgValue !== $actualValue) { $changeSet[$propName] = array($orgValue, $actualValue); } else { if ($orgValue != $actualValue || $orgValue === null ^ $actualValue === null) { $changeSet[$propName] = array($orgValue, $actualValue); } } if (isset($changeSet[$propName])) { if (isset($class->associationMappings[$propName])) { $assoc = $class->associationMappings[$propName]; if ($assoc->isOneToOne() && $assoc->isOwningSide) { $entityIsDirty = true; } else { if ($orgValue instanceof PersistentCollection) { // A PersistentCollection was de-referenced, so delete it. if (!in_array($orgValue, $this->_collectionDeletions, true)) { $this->_collectionDeletions[] = $orgValue; } } } } else { $entityIsDirty = true; } } } if ($changeSet) { if ($entityIsDirty) { $this->_entityUpdates[$oid] = $entity; } $this->_entityChangeSets[$oid] = $changeSet; $this->_originalEntityData[$oid] = $actualData; } } }
/** * Hydrates a single row in an SQL result set. * * @internal * First, the data of the row is split into chunks where each chunk contains data * that belongs to a particular component/class. Afterwards, all these chunks * are processed, one after the other. For each chunk of class data only one of the * following code paths is executed: * * Path A: The data chunk belongs to a joined/associated object and the association * is collection-valued. * Path B: The data chunk belongs to a joined/associated object and the association * is single-valued. * Path C: The data chunk belongs to a root result element/object that appears in the topmost * level of the hydrated result. A typical example are the objects of the type * specified by the FROM clause in a DQL query. * * @param array $data The data of the row to process. * @param array $cache The cache to use. * @param array $result The result array to fill. */ protected function _hydrateRow(array $data, array &$cache, array &$result) { // Initialize $id = $this->_idTemplate; // initialize the id-memory $nonemptyComponents = array(); // Split the row data into chunks of class data. $rowData = $this->_gatherRowData($data, $cache, $id, $nonemptyComponents); // Extract scalar values. They're appended at the end. if (isset($rowData['scalars'])) { $scalars = $rowData['scalars']; unset($rowData['scalars']); if (empty($rowData)) { ++$this->_resultCounter; } } // Hydrate the data chunks foreach ($rowData as $dqlAlias => $data) { $entityName = $this->_rsm->aliasMap[$dqlAlias]; if (isset($this->_rsm->parentAliasMap[$dqlAlias])) { // It's a joined result $parentAlias = $this->_rsm->parentAliasMap[$dqlAlias]; // we need the $path to save into the identifier map which entities were already // seen for this parent-child relationship $path = $parentAlias . '.' . $dqlAlias; // Get a reference to the parent object to which the joined element belongs. if ($this->_rsm->isMixed && isset($this->_rootAliases[$parentAlias])) { $first = reset($this->_resultPointers); $parentObject = $this->_resultPointers[$parentAlias][key($first)]; } else { if (isset($this->_resultPointers[$parentAlias])) { $parentObject = $this->_resultPointers[$parentAlias]; } else { // Parent object of relation not found, so skip it. continue; } } $parentClass = $this->_ce[$this->_rsm->aliasMap[$parentAlias]]; $oid = spl_object_hash($parentObject); $relationField = $this->_rsm->relationMap[$dqlAlias]; $relation = $parentClass->associationMappings[$relationField]; $reflField = $parentClass->reflFields[$relationField]; // Check the type of the relation (many or single-valued) if (!($relation['type'] & ClassMetadata::TO_ONE)) { // PATH A: Collection-valued association if (isset($nonemptyComponents[$dqlAlias])) { $collKey = $oid . $relationField; if (isset($this->_initializedCollections[$collKey])) { $reflFieldValue = $this->_initializedCollections[$collKey]; } else { if (!isset($this->_existingCollections[$collKey])) { $reflFieldValue = $this->_initRelatedCollection($parentObject, $parentClass, $relationField); } } $indexExists = isset($this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]); $index = $indexExists ? $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false; $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false; if (!$indexExists || !$indexIsValid) { if (isset($this->_existingCollections[$collKey])) { // Collection exists, only look for the element in the identity map. if ($element = $this->_getEntityFromIdentityMap($entityName, $data)) { $this->_resultPointers[$dqlAlias] = $element; } else { unset($this->_resultPointers[$dqlAlias]); } } else { $element = $this->_getEntity($data, $dqlAlias); if (isset($this->_rsm->indexByMap[$dqlAlias])) { $field = $this->_rsm->indexByMap[$dqlAlias]; $indexValue = $this->_ce[$entityName]->reflFields[$field]->getValue($element); $reflFieldValue->hydrateSet($indexValue, $element); $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue; } else { $reflFieldValue->hydrateAdd($element); $reflFieldValue->last(); $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key(); } // Update result pointer $this->_resultPointers[$dqlAlias] = $element; } } else { // Update result pointer $this->_resultPointers[$dqlAlias] = $reflFieldValue[$index]; } } else { if (!$reflField->getValue($parentObject)) { $coll = new PersistentCollection($this->_em, $this->_ce[$entityName], new ArrayCollection()); $coll->setOwner($parentObject, $relation); $reflField->setValue($parentObject, $coll); $this->_uow->setOriginalEntityProperty($oid, $relationField, $coll); } } } else { // PATH B: Single-valued association $reflFieldValue = $reflField->getValue($parentObject); if (!$reflFieldValue || isset($this->_hints[Query::HINT_REFRESH])) { if (isset($nonemptyComponents[$dqlAlias])) { $element = $this->_getEntity($data, $dqlAlias); $reflField->setValue($parentObject, $element); $this->_uow->setOriginalEntityProperty($oid, $relationField, $element); $targetClass = $this->_ce[$relation['targetEntity']]; if ($relation['isOwningSide']) { //TODO: Just check hints['fetched'] here? // If there is an inverse mapping on the target class its bidirectional if ($relation['inversedBy']) { $inverseAssoc = $targetClass->associationMappings[$relation['inversedBy']]; if ($inverseAssoc['type'] & ClassMetadata::TO_ONE) { $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($element, $parentObject); $this->_uow->setOriginalEntityProperty(spl_object_hash($element), $inverseAssoc['fieldName'], $parentObject); } } else { if ($parentClass === $targetClass && $relation['mappedBy']) { // Special case: bi-directional self-referencing one-one on the same class $targetClass->reflFields[$relationField]->setValue($element, $parentObject); } } } else { // For sure bidirectional, as there is no inverse side in unidirectional mappings $targetClass->reflFields[$relation['mappedBy']]->setValue($element, $parentObject); $this->_uow->setOriginalEntityProperty(spl_object_hash($element), $relation['mappedBy'], $parentObject); } // Update result pointer $this->_resultPointers[$dqlAlias] = $element; } // else leave $reflFieldValue null for single-valued associations } else { // Update result pointer $this->_resultPointers[$dqlAlias] = $reflFieldValue; } } } else { // PATH C: Its a root result element $this->_rootAliases[$dqlAlias] = true; // Mark as root alias if (!isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { $element = $this->_getEntity($rowData[$dqlAlias], $dqlAlias); if (isset($this->_rsm->indexByMap[$dqlAlias])) { $field = $this->_rsm->indexByMap[$dqlAlias]; $key = $this->_ce[$entityName]->reflFields[$field]->getValue($element); if ($this->_rsm->isMixed) { $element = array($key => $element); $result[] = $element; $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $this->_resultCounter; ++$this->_resultCounter; } else { $result[$key] = $element; $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $key; } if (isset($this->_hints['collection'])) { $this->_hints['collection']->hydrateSet($key, $element); } } else { if ($this->_rsm->isMixed) { $element = array(0 => $element); } $result[] = $element; $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $this->_resultCounter; ++$this->_resultCounter; if (isset($this->_hints['collection'])) { $this->_hints['collection']->hydrateAdd($element); } } // Update result pointer $this->_resultPointers[$dqlAlias] = $element; } else { // Update result pointer $index = $this->_identifierMap[$dqlAlias][$id[$dqlAlias]]; $this->_resultPointers[$dqlAlias] = $result[$index]; /*if ($this->_rsm->isMixed) { $result[] = $result[$index]; ++$this->_resultCounter; }*/ } } } // Append scalar values to mixed result sets if (isset($scalars)) { foreach ($scalars as $name => $value) { $result[$this->_resultCounter - 1][$name] = $value; } } }
/** * INTERNAL: * Creates an entity. Used for reconstitution of entities during hydration. * * @ignore * @param string $className The name of the entity class. * @param array $data The data for the entity. * @param array $hints Any hints to account for during reconstitution/lookup of the entity. * @return object The entity instance. * @internal Highly performance-sensitive method. * * @todo Rename: getOrCreateEntity */ public function createEntity($className, array $data, &$hints = array()) { $class = $this->_em->getClassMetadata($className); //$isReadOnly = isset($hints[Query::HINT_READ_ONLY]); if ($class->isIdentifierComposite) { $id = array(); foreach ($class->identifier as $fieldName) { $id[$fieldName] = $data[$fieldName]; } $idHash = implode(' ', $id); } else { $idHash = $data[$class->identifier[0]]; $id = array($class->identifier[0] => $idHash); } if (isset($this->_identityMap[$class->rootEntityName][$idHash])) { $entity = $this->_identityMap[$class->rootEntityName][$idHash]; $oid = spl_object_hash($entity); if ($entity instanceof Proxy && !$entity->__isInitialized__) { $entity->__isInitialized__ = true; $overrideLocalValues = true; } else { $overrideLocalValues = isset($hints[Query::HINT_REFRESH]); } } else { $entity = $class->newInstance(); $oid = spl_object_hash($entity); $this->_entityIdentifiers[$oid] = $id; $this->_entityStates[$oid] = self::STATE_MANAGED; $this->_originalEntityData[$oid] = $data; $this->_identityMap[$class->rootEntityName][$idHash] = $entity; if ($entity instanceof NotifyPropertyChanged) { $entity->addPropertyChangedListener($this); } $overrideLocalValues = true; } if ($overrideLocalValues) { if ($this->_useCExtension) { doctrine_populate_data($entity, $data); } else { foreach ($data as $field => $value) { if (isset($class->reflFields[$field])) { $class->reflFields[$field]->setValue($entity, $value); } } } // Properly initialize any unfetched associations, if partial objects are not allowed. if (!isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) { foreach ($class->associationMappings as $field => $assoc) { // Check if the association is not among the fetch-joined associations already. if (isset($hints['fetched'][$className][$field])) { continue; } $targetClass = $this->_em->getClassMetadata($assoc->targetEntityName); if ($assoc->isOneToOne()) { if ($assoc->isOwningSide) { $associatedId = array(); foreach ($assoc->targetToSourceKeyColumns as $targetColumn => $srcColumn) { $joinColumnValue = $data[$srcColumn]; if ($joinColumnValue !== null) { $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue; } } if (!$associatedId) { // Foreign key is NULL $class->reflFields[$field]->setValue($entity, null); $this->_originalEntityData[$oid][$field] = null; } else { // Foreign key is set // Check identity map first // FIXME: Can break easily with composite keys if join column values are in // wrong order. The correct order is the one in ClassMetadata#identifier. $relatedIdHash = implode(' ', $associatedId); if (isset($this->_identityMap[$targetClass->rootEntityName][$relatedIdHash])) { $newValue = $this->_identityMap[$targetClass->rootEntityName][$relatedIdHash]; } else { if ($targetClass->subClasses) { // If it might be a subtype, it can not be lazy $newValue = $assoc->load($entity, null, $this->_em, $associatedId); } else { $newValue = $this->_em->getProxyFactory()->getProxy($assoc->targetEntityName, $associatedId); $this->_entityIdentifiers[spl_object_hash($newValue)] = $associatedId; $this->_identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue; } } $this->_originalEntityData[$oid][$field] = $newValue; $class->reflFields[$field]->setValue($entity, $newValue); } } else { // Inverse side of x-to-one can never be lazy $class->reflFields[$field]->setValue($entity, $assoc->load($entity, null, $this->_em)); } } else { // Inject collection $reflField = $class->reflFields[$field]; $pColl = new PersistentCollection($this->_em, $targetClass, $reflField->getValue($entity) ?: new ArrayCollection()); $pColl->setOwner($entity, $assoc); $reflField->setValue($entity, $pColl); if ($assoc->isLazilyFetched()) { $pColl->setInitialized(false); } else { $assoc->load($entity, $pColl, $this->_em); } $this->_originalEntityData[$oid][$field] = $pColl; } } } } //TODO: These should be invoked later, after hydration, because associations may not yet be loaded here. if (isset($class->lifecycleCallbacks[Events::postLoad])) { $class->invokeLifecycleCallbacks(Events::postLoad, $entity); } if ($this->_evm->hasListeners(Events::postLoad)) { $this->_evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity)); } return $entity; }
/** * * @param object $entity * @param string $name * @todo Consider inlining this method. */ private function initRelatedCollection($entity, $name) { $oid = spl_object_hash($entity); $classMetadata = $this->_ce[get_class($entity)]; $relation = $classMetadata->associationMappings[$name]; $relatedClass = $this->_em->getClassMetadata($relation->targetEntityName); $coll = new PersistentCollection($this->_em, $relatedClass); $this->_collections[] = $coll; $coll->setOwner($entity, $relation); $classMetadata->reflFields[$name]->setValue($entity, $coll); $this->_uow->setOriginalEntityProperty($oid, $name, $coll); $this->_initializedRelations[$oid][$name] = true; return $coll; }