From e0488ff8fc4aeb0f75cfed6937781ee18bdbf5db Mon Sep 17 00:00:00 2001 From: romanb Date: Mon, 11 May 2009 10:43:27 +0000 Subject: [PATCH] [2.0] First draft of EntityManager#merge(). First draft of DynamicProxyGenerator. --- lib/Doctrine/Common/ClassLoader.php | 1 + lib/Doctrine/ORM/AbstractQuery.php | 2 +- lib/Doctrine/ORM/Configuration.php | 13 +- lib/Doctrine/ORM/DynamicProxyGenerator.php | 197 +++++++++++++++++ lib/Doctrine/ORM/EntityManager.php | 131 ++++++----- lib/Doctrine/ORM/EntityRepository.php | 26 ++- .../ORM/Internal/Hydration/ObjectHydrator.php | 1 + .../ORM/Mapping/AssociationMapping.php | 47 +++- lib/Doctrine/ORM/Mapping/ClassMetadata.php | 39 ++++ .../Mapping/Driver/DoctrineAnnotations.php | 6 + .../AbstractCollectionPersister.php | 2 +- lib/Doctrine/ORM/Query.php | 2 +- lib/Doctrine/ORM/UnitOfWork.php | 205 +++++++++++++----- lib/Doctrine/ORM/VirtualProxy.php | 2 +- tests/Doctrine/Tests/Models/CMS/CmsUser.php | 20 ++ .../Tests/ORM/Functional/AllTests.php | 1 + .../ORM/Functional/BasicFunctionalTest.php | 9 + .../ORM/Functional/DetachedEntityTest.php | 46 ++++ .../ORM/Hydration/ObjectHydratorTest.php | 36 +-- tests/Doctrine/Tests/TestInit.php | 10 - 20 files changed, 645 insertions(+), 151 deletions(-) create mode 100644 lib/Doctrine/ORM/DynamicProxyGenerator.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php diff --git a/lib/Doctrine/Common/ClassLoader.php b/lib/Doctrine/Common/ClassLoader.php index 36546c191..334e33d12 100644 --- a/lib/Doctrine/Common/ClassLoader.php +++ b/lib/Doctrine/Common/ClassLoader.php @@ -95,6 +95,7 @@ class ClassLoader $prefix = substr($className, 0, strpos($className, $this->_namespaceSeparator)); $class = ''; + if (isset($this->_basePaths[$prefix])) { $class .= $this->_basePaths[$prefix] . DIRECTORY_SEPARATOR; } diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 8045368f5..95230f6da 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -398,7 +398,7 @@ abstract class AbstractQuery if (count($result) > 1) { throw QueryException::nonUniqueResult(); } - return $result->getFirst(); + return $result->first(); } return $result; } diff --git a/lib/Doctrine/ORM/Configuration.php b/lib/Doctrine/ORM/Configuration.php index f04c8b987..45024ea45 100644 --- a/lib/Doctrine/ORM/Configuration.php +++ b/lib/Doctrine/ORM/Configuration.php @@ -45,10 +45,21 @@ class Configuration extends \Doctrine\DBAL\Configuration 'queryCacheImpl' => null, 'metadataCacheImpl' => null, 'metadataDriverImpl' => new AnnotationDriver(), - 'dqlClassAliasMap' => array() + 'dqlClassAliasMap' => array(), + 'cacheDir' => null )); } + public function setCacheDir($dir) + { + $this->_attributes['cacheDir'] = $dir; + } + + public function getCacheDir() + { + return $this->_attributes['cacheDir']; + } + public function getDqlClassAliasMap() { return $this->_attributes['dqlClassAliasMap']; diff --git a/lib/Doctrine/ORM/DynamicProxyGenerator.php b/lib/Doctrine/ORM/DynamicProxyGenerator.php new file mode 100644 index 000000000..f40ded06c --- /dev/null +++ b/lib/Doctrine/ORM/DynamicProxyGenerator.php @@ -0,0 +1,197 @@ +. + */ + +namespace Doctrine\ORM; + +/** + * The DynamicProxyGenerator is used to generate proxy objects for entities. + * For that purpose he generates proxy class files on the fly as needed. + * + * @author Roman Borschel + */ +class DynamicProxyGenerator +{ + private $_cacheDir = '/Users/robo/dev/php/tmp/gen/'; + private $_em; + + public function __construct(EntityManager $em, $cacheDir = null) + { + $this->_em = $em; + if ($cacheDir === null) { + $cacheDir = sys_get_tmp_dir(); + } + $this->_cacheDir = $cacheDir; + } + + /** + * + * + * @param $className + * @param $identifier + * @return + */ + public function getProxy($className, $identifier) + { + $proxyClassName = str_replace('\\', '_', $className) . 'Proxy'; + if ( ! class_exists($proxyClassName, false)) { + $fileName = $this->_cacheDir . $proxyClassName . '.g.php'; + if ( ! file_exists($fileName)) { + $this->_generateProxyClass($className, $identifier, $proxyClassName, $fileName); + } + require $fileName; + } + $proxyClassName = '\Doctrine\Generated\Proxies\\' . $proxyClassName; + return new $proxyClassName($this->_em, $this->_em->getClassMetadata($className), $identifier); + } + + /** + * Generates a proxy class. + * + * @param $className + * @param $id + * @param $proxyClassName + * @param $fileName + */ + private function _generateProxyClass($className, $id, $proxyClassName, $fileName) + { + $class = $this->_em->getClassMetadata($className); + $file = self::$_proxyClassTemplate; + + if (is_array($id) && count($id) > 1) { + // it's a composite key. keys = field names, values = values. + $values = array_values($id); + $keys = array_keys($id); + } else { + $values = is_array($id) ? array_values($id) : array($id); + $keys = $class->getIdentifierFieldNames(); + } + $paramIndex = 1; + $identifierCondition = 'prx.' . $keys[0] . ' = ?' . $paramIndex++; + for ($i=1, $c=count($keys); $i < $c; ++$i) { + $identifierCondition .= ' AND prx.' . $keys[$i] . ' = ?' . $paramIndex++; + } + + $parameters = 'array('; + $first = true; + foreach ($values as $value) { + if ($first) { + $first = false; + } else { + $parameters = ', '; + } + $parameters .= "'" . $value . "'"; + } + $parameters .= ')'; + + $hydrationSetters = ''; + foreach ($class->getReflectionProperties() as $name => $prop) { + if ( ! $class->hasAssociation($name)) { + $hydrationSetters .= '$this->_class->setValue($this, \'' . $name . '\', $scalar[0][\'prx_' . $name . '\']);' . PHP_EOL; + } + } + + $methods = ''; + foreach ($class->getReflectionClass()->getMethods() as $method) { + if ($method->isPublic() && ! $method->isFinal()) { + $methods .= PHP_EOL . 'public function ' . $method->getName() . '('; + $firstParam = true; + $parameterString = ''; + foreach ($method->getParameters() as $param) { + if ($firstParam) { + $firstParam = false; + } else { + $parameterString .= ', '; + } + $parameterString .= '$' . $param->getName(); + } + $methods .= $parameterString . ') {' . PHP_EOL; + $methods .= '$this->_load();' . PHP_EOL; + $methods .= 'return parent::' . $method->getName() . '(' . $parameterString . ');'; + $methods .= '}' . PHP_EOL; + } + } + + $sleepImpl = ''; + if ($class->getReflectionClass()->hasMethod('__sleep')) { + $sleepImpl .= 'return parent::__sleep();'; + } else { + $sleepImpl .= 'return array('; + $first = true; + foreach ($class->getReflectionProperties() as $name => $prop) { + if ($first) { + $first = false; + } else { + $sleepImpl .= ', '; + } + $sleepImpl .= "'" . $name . "'"; + } + $sleepImpl .= ');'; + } + + $placeholders = array( + '', '', '', + '', '', '', '' + ); + $replacements = array( + $proxyClassName, $className, $identifierCondition, $parameters, + $hydrationSetters, $methods, $sleepImpl + ); + + $file = str_replace($placeholders, $replacements, $file); + + file_put_contents($fileName, $file); + } + + /** Proxy class code template */ + private static $_proxyClassTemplate = +' extends \ { + private $_em; + private $_class; + private $_loaded = false; + public function __construct($em, $class, $identifier) { + $this->_em = $em; + $this->_class = $class; + $this->_class->setIdentifierValues($this, $identifier); + } + private function _load() { + if ( ! $this->_loaded) { + $scalar = $this->_em->createQuery(\'select prx from prx where \')->execute(, \Doctrine\ORM\Query::HYDRATE_SCALAR); + + unset($this->_em); + unset($this->_class); + $this->_loaded = true; + } + } + + + + public function __sleep() { + if (!$this->_loaded) { + throw new RuntimeException("Not fully loaded proxy can not be serialized."); + } + + } + } +}'; +} diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 2e58792b1..3341d5481 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -42,22 +42,22 @@ class EntityManager * IMMEDIATE: Flush occurs automatically after each operation that issues database * queries. No operations are queued. */ - const FLUSHMODE_IMMEDIATE = 'immediate'; + const FLUSHMODE_IMMEDIATE = 1; /** * AUTO: Flush occurs automatically in the following situations: * - Before any query executions (to prevent getting stale data) * - On EntityManager#commit() */ - const FLUSHMODE_AUTO = 'auto'; + const FLUSHMODE_AUTO = 2; /** * COMMIT: Flush occurs automatically only on EntityManager#commit(). */ - const FLUSHMODE_COMMIT = 'commit'; + const FLUSHMODE_COMMIT = 3; /** * MANUAL: Flush occurs never automatically. The only way to flush is * through EntityManager#flush(). */ - const FLUSHMODE_MANUAL = 'manual'; + const FLUSHMODE_MANUAL = 4; /** * The used Configuration. @@ -92,7 +92,7 @@ class EntityManager * * @var string */ - private $_flushMode = 'commit'; + private $_flushMode = self::FLUSHMODE_COMMIT; /** * The UnitOfWork used to coordinate object-level transactions. @@ -115,6 +115,13 @@ class EntityManager */ private $_hydrators = array(); + /** + * The proxy generator. + * + * @var DynamicProxyGenerator + */ + private $_proxyGenerator; + /** * Whether the EntityManager is closed or not. */ @@ -211,17 +218,6 @@ class EntityManager return $query; } - /** - * Detaches an entity from the manager. It's lifecycle is no longer managed. - * - * @param object $entity - * @return boolean - */ - public function detach($entity) - { - return $this->_unitOfWork->removeFromIdentityMap($entity); - } - /** * Creates a DQL query with the specified name. * @@ -237,6 +233,7 @@ class EntityManager * Creates a native SQL query. * * @param string $sql + * @param ResultSetMapping $rsm The ResultSetMapping to use. * @return Query */ public function createNativeQuery($sql, \Doctrine\ORM\Query\ResultSetMapping $rsm) @@ -287,6 +284,23 @@ class EntityManager { return $this->getRepository($entityName)->find($identifier); } + + /** + * Gets a reference to the entity identified by the given type and identifier + * without actually loading it. Only the identifier of the returned entity + * will be populated. + * + * NOTE: There is currently no magic proxying in place, that means the full state + * of the entity will not be loaded upon accessing it. + * + * @return object The entity reference. + */ + public function getReference($entityName, $identifier) + { + $entity = new $entityName; + $this->getClassMetadata($entityName)->setEntityIdentifier($entity, $identifier); + return $entity; + } /** * Sets the flush mode to use. @@ -309,10 +323,7 @@ class EntityManager */ private function _isFlushMode($value) { - return $value == self::FLUSHMODE_AUTO || - $value == self::FLUSHMODE_COMMIT || - $value == self::FLUSHMODE_IMMEDIATE || - $value == self::FLUSHMODE_MANUAL; + return $value >= 1 && $value <= 4; } /** @@ -326,7 +337,10 @@ class EntityManager } /** - * Clears the persistence context, effectively detaching all managed entities. + * Clears the EntityManager. All entities that are currently managed + * by this EntityManager become detached. + * + * @param string $entityName */ public function clear($entityName = null) { @@ -334,14 +348,18 @@ class EntityManager $this->_unitOfWork->detachAll(); } else { //TODO + throw DoctrineException::notImplemented(); } } /** - * Closes the EntityManager. + * Closes the EntityManager. All entities that are currently managed + * by this EntityManager become detached. The EntityManager may no longer + * be used after it is closed. */ public function close() { + $this->clear(); $this->_closed = true; } @@ -378,52 +396,51 @@ class EntityManager * overriding any local changes that have not yet been persisted. * * @param object $entity - * @todo FIX Impl + * @todo Implemntation */ public function refresh($entity) { + throw DoctrineException::notImplemented(); /*$this->_mergeData($entity, $this->getRepository(get_class($entity))->find( $entity->identifier(), Query::HYDRATE_ARRAY), true);*/ } + + /** + * Detaches an entity from the EntityManager. Its lifecycle is no longer managed. + * + * @param object $entity The entity to detach. + * @return boolean + */ + public function detach($entity) + { + return $this->_unitOfWork->removeFromIdentityMap($entity); + } + + /** + * Merges the state of a detached entity into the persistence context + * of this EntityManager. + * + * @param object $entity The entity to merge into the persistence context. + * @return object The managed copy of the entity. + */ + public function merge($entity) + { + return $this->_unitOfWork->merge($entity); + } /** * Creates a copy of the given entity. Can create a shallow or a deep copy. * * @param object $entity The entity to copy. * @return object The new entity. + * @todo Implementation or remove. */ public function copy($entity, $deep = false) { - //... + throw DoctrineException::notImplemented(); } -/* - public function toArray($entity, $deep = false) - { - $array = array(); - foreach ($entity as $key => $value) { - if ($deep && is_object($value)) { - $array[$key] = $this->toArray($value, $deep); - } else if ( ! is_object($value)) { - $array[$key] = $value; - } - } - return $array; - } - - public function fromArray($entity, array $array, $deep = false) - { - foreach ($array as $key => $value) { - if ($deep && is_array($value)) { - $entity->$key = $this->fromArray($entity, $value, $deep); - } else if ( ! is_array($value)) { - $entity->$key = $value; - } - } - } -*/ - /** * Gets the repository for an entity class. * @@ -528,11 +545,11 @@ class EntityManager $this->_hydrators[$hydrationMode] = new \Doctrine\ORM\Internal\Hydration\NoneHydrator($this); break; default: - \Doctrine\Common\DoctrineException::updateMe("No hydrator found for hydration mode '$hydrationMode'."); + throw DoctrineException::updateMe("No hydrator found for hydration mode '$hydrationMode'."); } - } else if ($this->_hydrators[$hydrationMode] instanceof Closure) { + }/* else if ($this->_hydrators[$hydrationMode] instanceof Closure) { $this->_hydrators[$hydrationMode] = $this->_hydrators[$hydrationMode]($this); - } + }*/ return $this->_hydrators[$hydrationMode]; } @@ -540,13 +557,13 @@ class EntityManager * Sets a hydrator for a hydration mode. * * @param mixed $hydrationMode - * @param object $hydrator Either a hydrator instance or a closure that creates a + * @param object $hydrator Either a hydrator instance or a Closure that creates a * hydrator instance. */ - public function setHydrator($hydrationMode, $hydrator) + /*public function setHydrator($hydrationMode, $hydrator) { $this->_hydrators[$hydrationMode] = $hydrator; - } + }*/ /** * Factory method to create EntityManager instances. @@ -563,7 +580,7 @@ class EntityManager if (is_array($conn)) { $conn = \Doctrine\DBAL\DriverManager::getConnection($conn, $config, $eventManager); } else if ( ! $conn instanceof Connection) { - \Doctrine\Common\DoctrineException::updateMe("Invalid parameter '$conn'."); + throw DoctrineException::updateMe("Invalid parameter '$conn'."); } if ($config === null) { diff --git a/lib/Doctrine/ORM/EntityRepository.php b/lib/Doctrine/ORM/EntityRepository.php index 7c0cbb13c..ed7b2908a 100644 --- a/lib/Doctrine/ORM/EntityRepository.php +++ b/lib/Doctrine/ORM/EntityRepository.php @@ -16,7 +16,7 @@ * * This software consists of voluntary contributions made by many individuals * and is licensed under the LGPL. For more information, see - * . + * . */ namespace Doctrine\ORM; @@ -80,10 +80,6 @@ class EntityRepository */ public function find($id, $hydrationMode = null) { - if ($id === null) { - return false; - } - if (is_array($id) && count($id) > 1) { // it's a composite key. keys = field names, values = values. $values = array_values($id); @@ -98,9 +94,21 @@ class EntityRepository return $entity; // Hit! } - return $this->_createQuery() - ->where(implode(' = ? AND ', $keys) . ' = ?') - ->fetchOne($values, $hydrationMode); + $dql = 'select e from ' . $this->_classMetadata->getClassName() . ' e where '; + $conditionDql = ''; + $paramIndex = 1; + foreach ($keys as $key) { + if ($conditionDql != '') $conditionDql .= ' and '; + $conditionDql .= 'e.' . $key . ' = ?' . $paramIndex++; + } + $dql .= $conditionDql; + + $q = $this->_em->createQuery($dql); + foreach ($values as $index => $value) { + $q->setParameter($index, $value); + } + + return $q->getSingleResult($hydrationMode); } /** @@ -172,7 +180,7 @@ class EntityRepository */ public function findByDql($dql, array $params = array(), $hydrationMode = null) { - $query = new Doctrine_Query($this->_em); + $query = new Query($this->_em); $component = $this->getComponentName(); $dql = 'FROM ' . $component . ' WHERE ' . $dql; diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index affda4a14..7b95fa29d 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -103,6 +103,7 @@ class ObjectHydrator extends AbstractHydrator $this->_classMetadatas = array(); $e = microtime(true); + echo 'Hydration took: ' . ($e - $s) . PHP_EOL; return $result; diff --git a/lib/Doctrine/ORM/Mapping/AssociationMapping.php b/lib/Doctrine/ORM/Mapping/AssociationMapping.php index 5961fcc2d..5ab486c19 100644 --- a/lib/Doctrine/ORM/Mapping/AssociationMapping.php +++ b/lib/Doctrine/ORM/Mapping/AssociationMapping.php @@ -50,6 +50,7 @@ abstract class AssociationMapping protected $_isCascadeDelete; protected $_isCascadeSave; protected $_isCascadeRefresh; + protected $_isCascadeMerge; /** * The fetch mode used for the association. @@ -207,6 +208,20 @@ abstract class AssociationMapping } return $this->_isCascadeRefresh; } + + /** + * Whether the association cascades merge() operations from the source entity + * to the target entity/entities. + * + * @return boolean + */ + public function isCascadeMerge() + { + if ($this->_isCascadeMerge === null) { + $this->_isCascadeMerge = in_array('merge', $this->_cascades); + } + return $this->_isCascadeMerge; + } /** * Whether the target entity/entities of the association are eagerly fetched. @@ -262,6 +277,7 @@ abstract class AssociationMapping * Whether the association is optional (0..X), or not (1..X). * * @return boolean TRUE if the association is optional, FALSE otherwise. + * @todo Only applicable to OneToOne. Move there. */ public function isOptional() { @@ -319,26 +335,51 @@ abstract class AssociationMapping { return $this->_mappedByFieldName; } - + + /** + * Whether the association is a one-to-one association. + * + * @return boolean + */ public function isOneToOne() { return false; } - + + /** + * Whether the association is a one-to-many association. + * + * @return boolean + */ public function isOneToMany() { return false; } - + + /** + * Whether the association is a many-to-many association. + * + * @return boolean + */ public function isManyToMany() { return false; } + /** + * Whether the association uses a join table for the mapping. + * + * @return boolean + */ public function usesJoinTable() { return (bool)$this->_joinTable; } + /** + * + * @param $entity + * @param $entityManager + */ abstract public function lazyLoadFor($entity, $entityManager); } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadata.php b/lib/Doctrine/ORM/Mapping/ClassMetadata.php index 066232b5b..9fc133748 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadata.php @@ -885,6 +885,45 @@ final class ClassMetadata $this->_reflectionProperties[$field]->setValue($entity, $value); } } + + /** + * Extracts the identifier values of an entity of this class. + * + * @param object $entity + * @return mixed + */ + public function getIdentifierValues($entity) + { + if ($this->_isIdentifierComposite) { + $id = array(); + foreach ($this->_identifier as $idField) { + $value = $this->_reflectionProperties[$idField]->getValue($entity); + if ($value !== null) { + $id[] = $value; + } + } + return $id; + } else { + return $this->_reflectionProperties[$this->_identifier[0]]->getValue($entity); + } + } + + /** + * Populates the entity identifier of an entity. + * + * @param object $entity + * @param mixed $id + */ + public function setIdentifierValues($entity, $id) + { + if ($this->_isIdentifierComposite) { + foreach ((array)$id as $idField => $idValue) { + $this->_reflectionProperties[$idField]->setValue($entity, $idValue); + } + } else { + $this->_reflectionProperties[$this->_identifier[0]]->setValue($entity, $id); + } + } /** * Gets all field mappings. diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index 087ade4d7..80b7b3de7 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -58,20 +58,26 @@ final class DoctrineOneToOne extends \Addendum\Annotation { public $targetEntity; public $mappedBy; public $cascade; + public $fetch; + public $optional; } final class DoctrineOneToMany extends \Addendum\Annotation { public $mappedBy; public $targetEntity; public $cascade; + public $fetch; } final class DoctrineManyToOne extends \Addendum\Annotation { public $targetEntity; public $cascade; + public $fetch; + public $optional; } final class DoctrineManyToMany extends \Addendum\Annotation { public $targetEntity; public $mappedBy; public $cascade; + public $fetch; } final class DoctrineElementCollection extends \Addendum\Annotation { public $tableName; diff --git a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php index 8b81c66d1..8228a6dd0 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php @@ -146,7 +146,7 @@ abstract class AbstractCollectionPersister abstract protected function _getUpdateRowSql(PersistentCollection $coll); /** - * Gets the SQL statement used for inserting a row from to the collection. + * Gets the SQL statement used for inserting a row in the collection. * * @param PersistentCollection $coll */ diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index e5834c1c0..296a816da 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -919,7 +919,7 @@ class Query extends AbstractQuery $this->_dqlParts[$queryPartName] = array($queryPart); } - $this->_state = Doctrine_ORM_Query::STATE_DIRTY; + $this->_state = self::STATE_DIRTY; return $this; } diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 0206501eb..8415bf2f8 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -398,12 +398,14 @@ class UnitOfWork implements PropertyChangedListener ) { //TODO: If $actualData[$name] is Collection then unwrap the array $assoc = $class->getAssociationMapping($name); - echo PHP_EOL . "INJECTING PCOLL into $name" . PHP_EOL; + //echo PHP_EOL . "INJECTING PCOLL into $name" . PHP_EOL; // Inject PersistentCollection $coll = new PersistentCollection($this->_em, $assoc->getTargetEntityName(), $actualData[$name] ? $actualData[$name] : array()); $coll->setOwner($entity, $assoc); - if ( ! $coll->isEmpty()) $coll->setDirty(true); + if ( ! $coll->isEmpty()) { + $coll->setDirty(true); + } $class->getReflectionProperty($name)->setValue($entity, $coll); $actualData[$name] = $coll; } @@ -428,7 +430,7 @@ class UnitOfWork implements PropertyChangedListener $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null; if (is_object($orgValue) && $orgValue !== $actualValue) { $changeSet[$propName] = array($orgValue, $actualValue); - } else if ($orgValue != $actualValue || ($orgValue === null xor $actualValue === null)) { + } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) { $changeSet[$propName] = array($orgValue, $actualValue); } @@ -764,8 +766,7 @@ class UnitOfWork implements PropertyChangedListener * Detaches an entity from the persistence management. It's persistence will * no longer be managed by Doctrine. * - * @param integer $oid object identifier - * @return boolean whether ot not the operation was successful + * @param object $entity The entity to detach. */ public function detach($entity) { @@ -802,7 +803,6 @@ class UnitOfWork implements PropertyChangedListener */ public function detachAll($entityName = null) { - //TODO: what do do with new/dirty/removed lists? $numDetached = 0; if ($entityName !== null && isset($this->_identityMap[$entityName])) { $numDetached = count($this->_identityMap[$entityName]); @@ -852,13 +852,20 @@ class UnitOfWork implements PropertyChangedListener /** * Gets the state of an entity within the current unit of work. * - * @param Doctrine\ORM\Entity $entity + * @param object $entity * @return int */ public function getEntityState($entity) { $oid = spl_object_hash($entity); if ( ! isset($this->_entityStates[$oid])) { + /*if (isset($this->_entityInsertions[$oid])) { + $this->_entityStates[$oid] = self::STATE_NEW; + } else if ( ! isset($this->_entityIdentifiers[$oid])) { + // Either NEW (if no ID) or DETACHED (if ID) + } else { + $this->_entityStates[$oid] = self::STATE_DETACHED; + }*/ if (isset($this->_entityIdentifiers[$oid]) && ! isset($this->_entityInsertions[$oid])) { $this->_entityStates[$oid] = self::STATE_DETACHED; } else { @@ -872,7 +879,7 @@ class UnitOfWork implements PropertyChangedListener * Removes an entity from the identity map. This effectively detaches the * entity from the persistence management of Doctrine. * - * @param Doctrine\ORM\Entity $entity + * @param object $entity * @return boolean */ public function removeFromIdentityMap($entity) @@ -1000,18 +1007,18 @@ class UnitOfWork implements PropertyChangedListener return; // Prevent infinite recursion } - $visited[$oid] = $entity; // mark visited + $visited[$oid] = $entity; // Mark visited $class = $this->_em->getClassMetadata(get_class($entity)); switch ($this->getEntityState($entity)) { case self::STATE_MANAGED: - // nothing to do, except if policy is "deferred explicit" + // Nothing to do, except if policy is "deferred explicit" if ($class->isChangeTrackingDeferredExplicit()) { $this->scheduleForDirtyCheck($entity); } break; case self::STATE_NEW: - //TODO: Better defer insert for post-insert ID generators also. + //TODO: Better defer insert for post-insert ID generators also? $idGen = $class->getIdGenerator(); if ($idGen->isPostInsertGenerator()) { $insertNow[$oid] = $entity; @@ -1020,22 +1027,19 @@ class UnitOfWork implements PropertyChangedListener $this->_entityStates[$oid] = self::STATE_MANAGED; if ( ! $idGen instanceof \Doctrine\ORM\Id\Assigned) { $this->_entityIdentifiers[$oid] = array($idValue); - $class->getSingleIdReflectionProperty()->setValue($entity, $idValue); + $class->setIdentifierValues($entity, $idValue); } else { $this->_entityIdentifiers[$oid] = $idValue; } } - //TODO: Calculate changeSet now instead of later to allow some optimizations - // in calculateChangeSets() (ie no need to consider NEW objects) ? $this->registerNew($entity); break; case self::STATE_DETACHED: throw DoctrineException::updateMe("Behavior of save() for a detached entity " . "is not yet defined."); case self::STATE_DELETED: - // entity becomes managed again + // Entity becomes managed again if ($this->isRegisteredRemoved($entity)) { - //TODO: better a method for this? unset($this->_entityDeletions[$oid]); } else { //FIXME: There's more to think of here... @@ -1093,6 +1097,93 @@ class UnitOfWork implements PropertyChangedListener $this->_cascadeDelete($entity, $visited); } + /** + * Merges the state of the given detached entity into this UnitOfWork. + * + * @param object $entity + * @return object The managed copy of the entity. + */ + public function merge($entity) + { + $visited = array(); + return $this->_doMerge($entity, $visited); + } + + /** + * Executes a merge operation on an entity. + * + * @param object $entity + * @param array $visited + * @return object The managed copy of the entity. + */ + private function _doMerge($entity, array &$visited, $prevManagedCopy = null, $assoc = null) + { + $class = $this->_em->getClassMetadata(get_class($entity)); + $id = $class->getIdentifierValues($entity); + + if ( ! $id) { + throw new InvalidArgumentException('New entity passed to merge().'); + } + + $managedCopy = $this->tryGetById($id, $class->getRootClassName()); + if ($managedCopy) { + if ($this->getEntityState($managedCopy) == self::STATE_DELETED) { + throw new InvalidArgumentException('Can not merge with a deleted entity.'); + } + } else { + $managedCopy = $this->_em->find($class->getClassName(), $id); + } + + // Merge state of $entity into existing (managed) entity + foreach ($class->getReflectionProperties() as $name => $prop) { + if ( ! $class->hasAssociation($name)) { + $prop->setValue($managedCopy, $prop->getValue($entity)); + } + if ($class->isChangeTrackingNotify()) { + //TODO + } + } + if ($class->isChangeTrackingDeferredExplicit()) { + //TODO + } + + if ($prevManagedCopy !== null) { + $assocField = $assoc->getSourceFieldName(); + $prevClass = $this->_em->getClassMetadata(get_class($prevManagedCopy)); + if ($assoc->isOneToOne()) { + $prevClass->getReflectionProperty($assocField)->setValue($prevManagedCopy, $managedCopy); + } else { + $prevClass->getReflectionProperty($assocField)->getValue($prevManagedCopy)->add($managedCopy); + } + } + + $this->_cascadeMerge($entity, $managedCopy, $visited); + + return $managedCopy; + } + + /** + * Cascades a merge operation to associated entities. + */ + private function _cascadeMerge($entity, $managedCopy, array &$visited) + { + $class = $this->_em->getClassMetadata(get_class($entity)); + foreach ($class->getAssociationMappings() as $assocMapping) { + if ( ! $assocMapping->isCascadeMerge()) { + continue; + } + $relatedEntities = $class->getReflectionProperty($assocMapping->getSourceFieldName()) + ->getValue($entity); + if (($relatedEntities instanceof Collection) && count($relatedEntities) > 0) { + foreach ($relatedEntities as $relatedEntity) { + $this->_doMerge($relatedEntity, $visited, $managedCopy, $assocMapping); + } + } else if (is_object($relatedEntities)) { + $this->_doMerge($relatedEntities, $visited, $managedCopy, $assocMapping); + } + } + } + /** * Cascades the save operation to associated entities. * @@ -1155,11 +1246,22 @@ class UnitOfWork implements PropertyChangedListener } /** - * Closes the UnitOfWork. + * Clears the UnitOfWork. */ - public function close() + public function clear() { - //... + $this->_identityMap = array(); + $this->_entityIdentifiers = array(); + $this->_originalEntityData = array(); + $this->_entityChangeSets = array(); + $this->_entityStates = array(); + $this->_scheduledForDirtyCheck = array(); + $this->_entityInsertions = array(); + $this->_entityUpdates = array(); + $this->_entityDeletions = array(); + $this->_collectionDeletions = array(); + $this->_collectionCreations = array(); + $this->_collectionUpdates = array(); $this->_commitOrderCalculator->clear(); } @@ -1221,8 +1323,8 @@ class UnitOfWork implements PropertyChangedListener $entity = $this->tryGetByIdHash($idHash, $class->getRootClassName()); if ($entity) { $oid = spl_object_hash($entity); - $this->_mergeData($entity, $data, $class/*, $query->getHint('doctrine.refresh')*/); - return $entity; + $overrideLocalChanges = false; + //$overrideLocalChanges = $query->getHint('doctrine.refresh'); } else { $entity = new $className; $oid = spl_object_hash($entity); @@ -1233,33 +1335,18 @@ class UnitOfWork implements PropertyChangedListener $prop->setValue($entity, new \Doctrine\ORM\VirtualProxy($entity, $lazyAssoc, $prop)); } }*/ - $this->_mergeData($entity, $data, $class, true); $this->_entityIdentifiers[$oid] = $id; $this->_entityStates[$oid] = self::STATE_MANAGED; + $this->_originalEntityData[$oid] = $data; $this->addToIdentityMap($entity); + $overrideLocalChanges = true; } - $this->_originalEntityData[$oid] = $data; - - return $entity; - } - - /** - * Merges the given data into the given entity, optionally overriding - * local changes. - * - * @param object $entity - * @param array $data - * @param boolean $overrideLocalChanges - * @todo Consider moving to ClassMetadata for a little performance improvement. - */ - private function _mergeData($entity, array $data, $class, $overrideLocalChanges = false) { if ($overrideLocalChanges) { foreach ($data as $field => $value) { $class->setValue($entity, $field, $value); } } else { - $oid = spl_object_hash($entity); foreach ($data as $field => $value) { if ($class->hasField($field)) { $currentValue = $class->getReflectionProperty($field)->getValue($entity); @@ -1270,6 +1357,8 @@ class UnitOfWork implements PropertyChangedListener } } } + + return $entity; } /** @@ -1300,9 +1389,9 @@ class UnitOfWork implements PropertyChangedListener /** * INTERNAL: - * For hydration purposes only. + * For internal purposes only. * - * Sets a property of the original data array of an entity. + * Sets a property value of the original data array of an entity. * * @param string $oid * @param string $property @@ -1328,7 +1417,13 @@ class UnitOfWork implements PropertyChangedListener } /** + * Tries to find an entity with the given identifier in the identity map of + * this UnitOfWork. * + * @param mixed $id The entity identifier to look for. + * @param string $rootClassName The name of the root class of the mapped entity hierarchy. + * @return mixed Returns the entity with the specified identifier if it exists in + * this UnitOfWork, FALSE otherwise. */ public function tryGetById($id, $rootClassName) { @@ -1429,23 +1524,25 @@ class UnitOfWork implements PropertyChangedListener */ public function propertyChanged($entity, $propertyName, $oldValue, $newValue) { - $oid = spl_object_hash($entity); - $class = $this->_em->getClassMetadata(get_class($entity)); - - $this->_entityChangeSets[$oid][$propertyName] = array($oldValue, $newValue); + if ($this->getEntityState($entity) == self::STATE_MANAGED) { + $oid = spl_object_hash($entity); + $class = $this->_em->getClassMetadata(get_class($entity)); - if ($class->hasAssociation($propertyName)) { - $assoc = $class->getAssociationMapping($name); - if ($assoc->isOneToOne() && $assoc->isOwningSide()) { - $this->_entityUpdates[$oid] = $entity; - } else if ($oldValue instanceof PersistentCollection) { - // A PersistentCollection was de-referenced, so delete it. - if ( ! in_array($orgValue, $this->_collectionDeletions, true)) { - $this->_collectionDeletions[] = $orgValue; + $this->_entityChangeSets[$oid][$propertyName] = array($oldValue, $newValue); + + if ($class->hasAssociation($propertyName)) { + $assoc = $class->getAssociationMapping($name); + if ($assoc->isOneToOne() && $assoc->isOwningSide()) { + $this->_entityUpdates[$oid] = $entity; + } else if ($oldValue instanceof PersistentCollection) { + // A PersistentCollection was de-referenced, so delete it. + if ( ! in_array($orgValue, $this->_collectionDeletions, true)) { + $this->_collectionDeletions[] = $orgValue; + } } + } else { + $this->_entityUpdates[$oid] = $entity; } - } else { - $this->_entityUpdates[$oid] = $entity; } } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/VirtualProxy.php b/lib/Doctrine/ORM/VirtualProxy.php index 02921de9a..42eb0afb2 100644 --- a/lib/Doctrine/ORM/VirtualProxy.php +++ b/lib/Doctrine/ORM/VirtualProxy.php @@ -26,7 +26,7 @@ use Doctrine\ORM\Mapping\AssociationMapping; /** * Represents a virtual proxy that is used for lazy to-one associations. * - * @author robo + * @author Roman Borschel * @since 2.0 */ class VirtualProxy diff --git a/tests/Doctrine/Tests/Models/CMS/CmsUser.php b/tests/Doctrine/Tests/Models/CMS/CmsUser.php index 8209db002..9ce7901b6 100644 --- a/tests/Doctrine/Tests/Models/CMS/CmsUser.php +++ b/tests/Doctrine/Tests/Models/CMS/CmsUser.php @@ -46,6 +46,22 @@ class CmsUser */ public $groups; + public function getId() { + return $this->id; + } + + public function getStatus() { + return $this->status; + } + + public function getUsername() { + return $this->username; + } + + public function getName() { + return $this->name; + } + /** * Adds a phonenumber to the user. * @@ -58,6 +74,10 @@ class CmsUser } } + public function getPhonenumbers() { + return $this->phonenumbers; + } + public function addArticle(CmsArticle $article) { $this->articles[] = $article; if ($article->user !== $this) { diff --git a/tests/Doctrine/Tests/ORM/Functional/AllTests.php b/tests/Doctrine/Tests/ORM/Functional/AllTests.php index 5cab14775..a1e833f0d 100644 --- a/tests/Doctrine/Tests/ORM/Functional/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Functional/AllTests.php @@ -22,6 +22,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Functional\BasicFunctionalTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\NativeQueryTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\SingleTableInheritanceTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Functional\DetachedEntityTest'); return $suite; } diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index 25c2eeecf..09394a024 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -306,5 +306,14 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $query = $this->_em->createQuery("select u, g from Doctrine\Tests\Models\CMS\CmsUser u inner join u.groups g"); $this->assertEquals(0, count($query->getResultList())); + + /* RB: TEST + \Doctrine\ORM\DynamicProxyGenerator::configure($this->_em); + $proxy = \Doctrine\ORM\DynamicProxyGenerator::getReferenceProxy('Doctrine\Tests\Models\CMS\CmsUser', 1); + echo $proxy->getId(); + var_dump(serialize($proxy)); + */ + + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php b/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php new file mode 100644 index 000000000..df8ff2555 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php @@ -0,0 +1,46 @@ +useModelSet('cms'); + parent::setUp(); + } + + public function testSimpleDetachMerge() { + $user = new CmsUser; + $user->name = 'Roman'; + $user->username = 'romanb'; + $user->status = 'dev'; + $this->_em->save($user); + $this->_em->flush(); + $this->_em->clear(); + + // $user is now detached + + $this->assertFalse($this->_em->contains($user)); + + $user->name = 'Roman B.'; + + //$this->assertEquals(UnitOfWork::STATE_DETACHED, $this->_em->getUnitOfWork()->getEntityState($user)); + + $user2 = $this->_em->merge($user); + + $this->assertFalse($user === $user2); + $this->assertTrue($this->_em->contains($user2)); + $this->assertEquals('Roman B.', $user2->name); + } +} + diff --git a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php index 34ef8aea6..9af62780e 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php @@ -693,16 +693,18 @@ class ObjectHydratorTest extends HydrationTest { $rsm = new ResultSetMapping; $rsm->addEntityResult($this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'), 'u'); - $rsm->addJoinedEntityResult( + /*$rsm->addJoinedEntityResult( $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsPhonenumber'), 'p', 'u', $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser')->getAssociationMapping('phonenumbers') - ); + );*/ $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); - $rsm->addScalarResult('sclr0', 'nameUpper'); - $rsm->addFieldResult('p', 'p__phonenumber', 'phonenumber'); + $rsm->addFieldResult('u', 'u__username', 'username'); + $rsm->addFieldResult('u', 'u__name', 'name'); + //$rsm->addScalarResult('sclr0', 'nameUpper'); + //$rsm->addFieldResult('p', 'p__phonenumber', 'phonenumber'); // Faked result set $resultSet = array( @@ -710,29 +712,37 @@ class ObjectHydratorTest extends HydrationTest array( 'u__id' => '1', 'u__status' => 'developer', - 'sclr0' => 'ROMANB', - 'p__phonenumber' => '42', + 'u__username' => 'romanb', + 'u__name' => 'Roman', + //'sclr0' => 'ROMANB', + //'p__phonenumber' => '42', ), array( 'u__id' => '1', 'u__status' => 'developer', - 'sclr0' => 'ROMANB', - 'p__phonenumber' => '43', + 'u__username' => 'romanb', + 'u__name' => 'Roman', + //'sclr0' => 'ROMANB', + //'p__phonenumber' => '43', ), array( 'u__id' => '2', 'u__status' => 'developer', - 'sclr0' => 'JWAGE', - 'p__phonenumber' => '91' + 'u__username' => 'romanb', + 'u__name' => 'Roman', + //'sclr0' => 'JWAGE', + //'p__phonenumber' => '91' ) ); - for ($i = 4; $i < 300; $i++) { + for ($i = 4; $i < 1000; ++$i) { $resultSet[] = array( 'u__id' => $i, 'u__status' => 'developer', - 'sclr0' => 'JWAGE' . $i, - 'p__phonenumber' => '91' + 'u__username' => 'jwage', + 'u__name' => 'Jonathan', + //'sclr0' => 'JWAGE' . $i, + //'p__phonenumber' => '91' ); } diff --git a/tests/Doctrine/Tests/TestInit.php b/tests/Doctrine/Tests/TestInit.php index b2259f015..5af6d8f75 100644 --- a/tests/Doctrine/Tests/TestInit.php +++ b/tests/Doctrine/Tests/TestInit.php @@ -20,13 +20,3 @@ set_include_path( . PATH_SEPARATOR . $modelDir . DIRECTORY_SEPARATOR . 'forum' ); -// Some of these classes depend on Doctrine_* classes -/*require_once 'DoctrineTestCase.php'; -require_once 'TestUtil.php'; -require_once 'DbalTestCase.php'; -require_once 'OrmTestCase.php'; -require_once 'OrmFunctionalTestCase.php'; -require_once 'DoctrineTestSuite.php'; -require_once 'OrmTestSuite.php'; -require_once 'OrmFunctionalTestSuite.php'; -require_once 'DbalTestSuite.php';*/