diff --git a/README.markdown b/README.markdown index e279f5f06..a0b5f2a20 100644 --- a/README.markdown +++ b/README.markdown @@ -1,6 +1,6 @@ # Doctrine 2 ORM -Doctrine 2 is an object-relational mapper (ORM) for PHP 5.3.0+ that provides transparent persistence +Doctrine 2 is an object-relational mapper (ORM) for PHP 5.3.2+ that provides transparent persistence for PHP objects. It sits on top of a powerful database abstraction layer (DBAL). One of its key features is the option to write database queries in a proprietary object oriented SQL dialect called Doctrine Query Language (DQL), inspired by Hibernates HQL. This provides developers with a powerful alternative to SQL that maintains flexibility diff --git a/UPGRADE_TO_2_2 b/UPGRADE_TO_2_2 index 76372b221..220fb39f9 100644 --- a/UPGRADE_TO_2_2 +++ b/UPGRADE_TO_2_2 @@ -1,3 +1,14 @@ +# EntityManager#getPartialReference() creates read-only entity + +Entities returned from EntityManager#getPartialReference() are now marked as read-only if they +haven't been in the identity map before. This means objects of this kind never lead to changes +in the UnitOfWork. + +# Fields omitted in a partial DQL query or a native query are never updated + +Fields of an entity that are not returned from a partial DQL Query or native SQL query +will never be updated through an UPDATE statement. + # Removed support for onUpdate in @JoinColumn The onUpdate foreign key handling makes absolutly no sense in an ORM. Additionally Oracle doesn't even support it. Support for it is removed. diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..080885107 --- /dev/null +++ b/composer.json @@ -0,0 +1,20 @@ +{ + "name": "doctrine/orm", + "type": "library", + "description": "Object-Relational-Mapper for PHP", + "keywords": ["orm", "database"], + "homepage": "http://www.doctrine-project.org", + "license": "LGPL", + "authors": [ + {"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"}, + {"name": "Roman Borschel", "email": "roman@code-factory.org"}, + {"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"}, + {"name": "Jonathan Wage", "email": "jonwage@gmail.com"} + ], + "require": { + "php": ">=5.3.2", + "ext-pdo": "*", + "doctrine/common": "master-dev", + "doctrine/dbal": "master-dev" + } +} diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index 627ddf460..e8e21f193 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -150,7 +150,7 @@ - + diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 9be5b50da..b3583de8e 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -128,7 +128,7 @@ class EntityManager implements ObjectManager $this->metadataFactory = new $metadataFactoryClassName; $this->metadataFactory->setEntityManager($this); $this->metadataFactory->setCacheDriver($this->config->getMetadataCacheImpl()); - + $this->unitOfWork = new UnitOfWork($this); $this->proxyFactory = new ProxyFactory($this, $config->getProxyDir(), @@ -203,18 +203,18 @@ class EntityManager implements ObjectManager public function transactional(Closure $func) { $this->conn->beginTransaction(); - + try { $return = $func($this); - + $this->flush(); $this->conn->commit(); - + return $return ?: true; } catch (Exception $e) { $this->close(); $this->conn->rollback(); - + throw $e; } } @@ -244,7 +244,7 @@ class EntityManager implements ObjectManager * * The class name must be the fully-qualified class name without a leading backslash * (as it is returned by get_class($obj)) or an aliased class name. - * + * * Examples: * MyProject\Domain\User * sales:PriceRequest @@ -325,13 +325,17 @@ class EntityManager implements ObjectManager * This effectively synchronizes the in-memory state of managed objects with the * database. * + * If an entity is explicitly passed to this method only this entity and + * the cascade-persist semantics + scheduled inserts/removals are synchronized. + * + * @param object $entity * @throws Doctrine\ORM\OptimisticLockException If a version check on an entity that * makes use of optimistic locking fails. */ - public function flush() + public function flush($entity = null) { $this->errorIfClosed(); - $this->unitOfWork->commit(); + $this->unitOfWork->commit($entity); } /** @@ -413,6 +417,7 @@ class EntityManager implements ObjectManager $entity = $class->newInstance(); $class->setIdentifierValues($entity, $identifier); $this->unitOfWork->registerManaged($entity, $identifier, array()); + $this->unitOfWork->markReadOnly($entity); return $entity; } @@ -421,16 +426,11 @@ class EntityManager implements ObjectManager * Clears the EntityManager. All entities that are currently managed * by this EntityManager become detached. * - * @param string $entityName + * @param string $entityName if given, only entities of this type will get detached */ public function clear($entityName = null) { - if ($entityName === null) { - $this->unitOfWork->clear(); - } else { - //TODO - throw new ORMException("EntityManager#clear(\$entityName) not yet implemented."); - } + $this->unitOfWork->clear($entityName); } /** @@ -449,7 +449,7 @@ class EntityManager implements ObjectManager * * The entity will be entered into the database at or before transaction * commit or as a result of the flush operation. - * + * * NOTE: The persist operation always considers entities that are not yet known to * this EntityManager as NEW. Do not pass detached entities to the persist operation. * @@ -632,7 +632,7 @@ class EntityManager implements ObjectManager /** * Check if the Entity manager is open or closed. - * + * * @return bool */ public function isOpen() @@ -713,6 +713,18 @@ class EntityManager implements ObjectManager return $this->proxyFactory; } + /** + * Helper method to initialize a lazy loading proxy or persistent collection. + * + * This method is a no-op for other objects + * + * @param object $obj + */ + public function initializeObject($obj) + { + $this->unitOfWork->initializeObject($obj); + } + /** * Factory method to create EntityManager instances. * diff --git a/lib/Doctrine/ORM/EntityRepository.php b/lib/Doctrine/ORM/EntityRepository.php index de2689db2..9760a1c42 100644 --- a/lib/Doctrine/ORM/EntityRepository.php +++ b/lib/Doctrine/ORM/EntityRepository.php @@ -178,7 +178,7 @@ class EntityRepository implements ObjectRepository */ public function findOneBy(array $criteria) { - return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->load($criteria); + return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->load($criteria, null, null, array(), 0, 1); } /** @@ -204,8 +204,7 @@ class EntityRepository implements ObjectRepository ); } - if ( !isset($arguments[0])) { - // we dont even want to allow null at this point, because we cannot (yet) transform it into IS NULL. + if (empty($arguments)) { throw ORMException::findByRequiresParameter($method.$by); } diff --git a/lib/Doctrine/ORM/Event/EntityEventDelegator.php b/lib/Doctrine/ORM/Event/EntityEventDelegator.php new file mode 100644 index 000000000..d7c46e68e --- /dev/null +++ b/lib/Doctrine/ORM/Event/EntityEventDelegator.php @@ -0,0 +1,110 @@ +. +*/ + +namespace Doctrine\ORM\Event; + +use \Doctrine\Common\EventSubscriber; +use \LogicException; + +/** + * Delegate events only for certain entities they are registered for. + * + * @author Benjamin Eberlei + * @since 2.2 + */ +class EntityEventDelegator implements EventSubscriber +{ + /** + * Keeps track of all the event listeners. + * + * @var array + */ + private $listeners = array(); + + /** + * If frozen no new event listeners can be added. + * + * @var bool + */ + private $frozen = false; + + /** + * Adds an event listener that listens on the specified events. + * + * @param string|array $events The event(s) to listen on. + * @param string|array $entities The entities to trigger this listener for + * @param object $listener The listener object. + */ + public function addEventListener($events, $entities, $listener) + { + if ($this->frozen) { + throw new LogicException("Cannot add event listeners after EntityEventDelegator::getSubscribedEvents() " . + "is called once. This happens when you register the delegator with the event manager."); + } + + // Picks the hash code related to that listener + $hash = spl_object_hash($listener); + + foreach ((array) $events as $event) { + // Overrides listener if a previous one was associated already + // Prevents duplicate listeners on same event (same instance only) + $this->listeners[$event][$hash] = array('listener' => $listener, 'entities' => array_flip((array)$entities)); + } + } + + /** + * Adds an EventSubscriber. The subscriber is asked for all the events he is + * interested in and added as a listener for these events. + * + * @param Doctrine\Common\EventSubscriber $subscriber The subscriber. + */ + public function addEventSubscriber(EventSubscriber $subscriber, $entities) + { + $this->addEventListener($subscriber->getSubscribedEvents(), $entities, $subscriber); + } + + /** + * Returns an array of events this subscriber wants to listen to. + * + * @return array + */ + public function getSubscribedEvents() + { + $this->frozen = true; + return array_keys($this->listeners); + } + + /** + * Delegate the event to an appropriate listener + * + * @param $eventName + * @param $event + * @return void + */ + public function __call($eventName, $args) + { + $event = $args[0]; + foreach ($this->listeners[$eventName] AS $listenerData) { + $class = get_class($event->getEntity()); + if (isset($listenerData['entities'][$class])) { + $listenerData['listener']->$eventName($event); + } + } + } +} diff --git a/lib/Doctrine/ORM/Event/OnClearEventArgs.php b/lib/Doctrine/ORM/Event/OnClearEventArgs.php index ad89fbc90..60ce4b3eb 100644 --- a/lib/Doctrine/ORM/Event/OnClearEventArgs.php +++ b/lib/Doctrine/ORM/Event/OnClearEventArgs.php @@ -36,12 +36,18 @@ class OnClearEventArgs extends \Doctrine\Common\EventArgs */ private $em; + /** + * @var string + */ + private $entityClass; + /** * @param \Doctrine\ORM\EntityManager $em */ - public function __construct($em) + public function __construct($em, $entityClass = null) { $this->em = $em; + $this->entityClass = $entityClass; } /** @@ -51,4 +57,24 @@ class OnClearEventArgs extends \Doctrine\Common\EventArgs { return $this->em; } + + /** + * Name of the entity class that is cleared, or empty if all are cleared. + * + * @return string + */ + public function getEntityClass() + { + return $this->entityClass; + } + + /** + * Check if event clears all entities. + * + * @return bool + */ + public function clearsAllEntities() + { + return $this->entityClass === null; + } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Event/PostFlushEventArgs.php b/lib/Doctrine/ORM/Event/PostFlushEventArgs.php new file mode 100644 index 000000000..92e88ae21 --- /dev/null +++ b/lib/Doctrine/ORM/Event/PostFlushEventArgs.php @@ -0,0 +1,57 @@ +. +*/ + +namespace Doctrine\ORM\Event; +use Doctrine\ORM\EntityManager; +use Doctrine\Common\EventArgs; + +/** + * Provides event arguments for the postFlush event. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @version $Revision$ + * @author Daniel Freudenberger + */ +class PostFlushEventArgs extends EventArgs +{ + /** + * @var EntityManager + */ + private $em; + + /** + * @param EntityManager $em + */ + public function __construct(EntityManager $em) + { + $this->em = $em; + } + + /** + * @return EntityManager + */ + public function getEntityManager() + { + return $this->em; + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Event/PreUpdateEventArgs.php b/lib/Doctrine/ORM/Event/PreUpdateEventArgs.php index c61e26d4c..ab1cc15de 100644 --- a/lib/Doctrine/ORM/Event/PreUpdateEventArgs.php +++ b/lib/Doctrine/ORM/Event/PreUpdateEventArgs.php @@ -90,7 +90,7 @@ class PreUpdateEventArgs extends LifecycleEventArgs if (!isset($this->_entityChangeSet[$field])) { throw new \InvalidArgumentException( "Field '".$field."' is not a valid field of the entity ". - "'".get_class($this->getEntity())."' in PreInsertUpdateEventArgs." + "'".get_class($this->getEntity())."' in PreUpdateEventArgs." ); } } diff --git a/lib/Doctrine/ORM/Events.php b/lib/Doctrine/ORM/Events.php index 8344b07c1..e8c350aa6 100644 --- a/lib/Doctrine/ORM/Events.php +++ b/lib/Doctrine/ORM/Events.php @@ -120,6 +120,17 @@ final class Events */ const onFlush = 'onFlush'; + /** + * The postFlush event occurs when the EntityManager#flush() operation is invoked and + * after all actual database operations are executed successfully. The event is only raised if there is + * actually something to do for the underlying UnitOfWork. If nothing needs to be done, + * the postFlush event is not raised. The event won't be raised if an error occurs during the + * flush operation. + * + * @var string + */ + const postFlush = 'postFlush'; + /** * The onClear event occurs when the EntityManager#clear() operation is invoked, * after all references to entities have been removed from the unit of work. diff --git a/lib/Doctrine/ORM/Id/AssignedGenerator.php b/lib/Doctrine/ORM/Id/AssignedGenerator.php index 05c3790af..90a35fa12 100644 --- a/lib/Doctrine/ORM/Id/AssignedGenerator.php +++ b/lib/Doctrine/ORM/Id/AssignedGenerator.php @@ -53,14 +53,14 @@ class AssignedGenerator extends AbstractIdGenerator if (!$em->getUnitOfWork()->isInIdentityMap($value)) { throw ORMException::entityMissingForeignAssignedId($entity, $value); } - + // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced. $identifier[$idField] = current($em->getUnitOfWork()->getEntityIdentifier($value)); } else { $identifier[$idField] = $value; } } else { - throw ORMException::entityMissingAssignedId($entity); + throw ORMException::entityMissingAssignedIdForField($entity, $idField); } } } else { @@ -71,17 +71,17 @@ class AssignedGenerator extends AbstractIdGenerator if (!$em->getUnitOfWork()->isInIdentityMap($value)) { throw ORMException::entityMissingForeignAssignedId($entity, $value); } - + // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced. $identifier[$idField] = current($em->getUnitOfWork()->getEntityIdentifier($value)); } else { $identifier[$idField] = $value; } } else { - throw ORMException::entityMissingAssignedId($entity); + throw ORMException::entityMissingAssignedIdForField($entity, $idField); } } - + return $identifier; } -} \ No newline at end of file +} diff --git a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php index 181854e36..5899a69ca 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php @@ -164,6 +164,11 @@ abstract class AbstractHydrator * field names during this procedure as well as any necessary conversions on * the values applied. * + * @param array $data SQL Result Row + * @param array &$cache Cache for column to field result information + * @param array &$id Dql-Alias => ID-Hash + * @param array &$nonemptyComponents Does this DQL-Alias has at least one non NULL value? + * * @return array An array with all the fields (name => value) of the data row, * grouped by their component alias. */ diff --git a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php index 92eb45c5c..4b1c21c6f 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php @@ -92,6 +92,11 @@ class ArrayHydrator extends AbstractHydrator $parent = $this->_rsm->parentAliasMap[$dqlAlias]; $path = $parent . '.' . $dqlAlias; + // missing parent data, skipping as RIGHT JOIN hydration is not supported. + if ( ! isset($nonemptyComponents[$parent]) ) { + continue; + } + // Get a reference to the right element in the result tree. // This element will get the associated element attached. if ($this->_rsm->isMixed && isset($this->_rootAliases[$parent])) { @@ -154,6 +159,17 @@ class ArrayHydrator extends AbstractHydrator // It's a root result element $this->_rootAliases[$dqlAlias] = true; // Mark as root + + // if this row has a NULL value for the root result id then make it a null result. + if ( ! isset($nonemptyComponents[$dqlAlias]) ) { + if ($this->_rsm->isMixed) { + $result[] = array(0 => null); + } else { + $result[] = null; + } + ++$this->_resultCounter; + continue; + } // Check for an existing element if ($this->_isSimpleQuery || ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index bb11a7431..1287a138b 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -302,6 +302,12 @@ class ObjectHydrator extends AbstractHydrator // seen for this parent-child relationship $path = $parentAlias . '.' . $dqlAlias; + // We have a RIGHT JOIN result here. Doctrine cannot hydrate RIGHT JOIN Object-Graphs + if (!isset($nonemptyComponents[$parentAlias])) { + // TODO: Add special case code where we hydrate the right join objects into identity map at least + continue; + } + // 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); @@ -408,6 +414,18 @@ class ObjectHydrator extends AbstractHydrator // PATH C: Its a root result element $this->_rootAliases[$dqlAlias] = true; // Mark as root alias + // if this row has a NULL value for the root result id then make it a null result. + if ( ! isset($nonemptyComponents[$dqlAlias]) ) { + if ($this->_rsm->isMixed) { + $result[] = array(0 => null); + } else { + $result[] = null; + } + ++$this->_resultCounter; + continue; + } + + // check for existing result from the iterations before if ( ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { $element = $this->_getEntity($rowData[$dqlAlias], $dqlAlias); if (isset($this->_rsm->indexByMap[$dqlAlias])) { diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index cef558496..239022b0f 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -50,7 +50,7 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface private $targetPlatform; /** - * @var Driver\Driver + * @var \Doctrine\ORM\Mapping\Driver\Driver */ private $driver; @@ -308,6 +308,10 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface if ($parent && $parent->isInheritanceTypeSingleTable()) { $class->setPrimaryTable($parent->table); } + + if ($parent && $parent->containsForeignIdentifier) { + $class->containsForeignIdentifier = true; + } $class->setParentClasses($visited); @@ -481,4 +485,19 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface throw new ORMException("Unknown generator type: " . $class->generatorType); } } + + /** + * Check if this class is mapped by this EntityManager + ClassMetadata configuration + * + * @param $class + * @return bool + */ + public function isTransient($class) + { + if ( ! $this->initialized) { + $this->initialize(); + } + + return $this->driver->isTransient($class); + } } diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 17255cb23..25a3350d2 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -318,7 +318,7 @@ class ClassMetadataInfo implements ClassMetadata public $discriminatorMap = array(); /** - * READ-ONLY: The definition of the descriminator column used in JOINED and SINGLE_TABLE + * READ-ONLY: The definition of the discriminator column used in JOINED and SINGLE_TABLE * inheritance mappings. * * @var array diff --git a/lib/Doctrine/ORM/Mapping/Driver/SimplifiedXmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/SimplifiedXmlDriver.php new file mode 100644 index 000000000..e60eab779 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/Driver/SimplifiedXmlDriver.php @@ -0,0 +1,176 @@ +. +*/ + +namespace Doctrine\ORM\Mapping\Driver; + +use Doctrine\ORM\Mapping\MappingException; + +/** + * XmlDriver that additionally looks for mapping information in a global file. + * + * @author Fabien Potencier + * @author Benjamin Eberlei + * @license MIT + */ +class SimplifiedXmlDriver extends XmlDriver +{ + protected $_prefixes = array(); + protected $_globalBasename; + protected $_classCache; + protected $_fileExtension = '.orm.xml'; + + public function __construct($prefixes) + { + $this->addNamespacePrefixes($prefixes); + } + + public function setGlobalBasename($file) + { + $this->_globalBasename = $file; + } + + public function getGlobalBasename() + { + return $this->_globalBasename; + } + + public function addNamespacePrefixes($prefixes) + { + $this->_prefixes = array_merge($this->_prefixes, $prefixes); + $this->addPaths(array_flip($prefixes)); + } + + public function getNamespacePrefixes() + { + return $this->_prefixes; + } + + public function isTransient($className) + { + if (null === $this->_classCache) { + $this->initialize(); + } + + // The mapping is defined in the global mapping file + if (isset($this->_classCache[$className])) { + return false; + } + + try { + $this->_findMappingFile($className); + + return false; + } catch (MappingException $e) { + return true; + } + } + + public function getAllClassNames() + { + if (null === $this->_classCache) { + $this->initialize(); + } + + $classes = array(); + + if ($this->_paths) { + foreach ((array) $this->_paths as $path) { + if (!is_dir($path)) { + throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($path), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) { + $fileName = $file->getBasename($this->_fileExtension); + + if ($fileName == $file->getBasename() || $fileName == $this->_globalBasename) { + continue; + } + + // NOTE: All files found here means classes are not transient! + if (isset($this->_prefixes[$path])) { + $classes[] = $this->_prefixes[$path].'\\'.str_replace('.', '\\', $fileName); + } else { + $classes[] = str_replace('.', '\\', $fileName); + } + } + } + } + + return array_merge($classes, array_keys($this->_classCache)); + } + + public function getElement($className) + { + if (null === $this->_classCache) { + $this->initialize(); + } + + if (!isset($this->_classCache[$className])) { + $this->_classCache[$className] = parent::getElement($className); + } + + return $this->_classCache[$className]; + } + + protected function initialize() + { + $this->_classCache = array(); + if (null !== $this->_globalBasename) { + foreach ($this->_paths as $path) { + if (is_file($file = $path.'/'.$this->_globalBasename.$this->_fileExtension)) { + $this->_classCache = array_merge($this->_classCache, $this->_loadMappingFile($file)); + } + } + } + } + + protected function _findMappingFile($className) + { + $defaultFileName = str_replace('\\', '.', $className).$this->_fileExtension; + foreach ($this->_paths as $path) { + if (!isset($this->_prefixes[$path])) { + if (is_file($path.DIRECTORY_SEPARATOR.$defaultFileName)) { + return $path.DIRECTORY_SEPARATOR.$defaultFileName; + } + + continue; + } + + $prefix = $this->_prefixes[$path]; + + if (0 !== strpos($className, $prefix.'\\')) { + continue; + } + + $filename = $path.'/'.strtr(substr($className, strlen($prefix)+1), '\\', '.').$this->_fileExtension; + if (is_file($filename)) { + return $filename; + } + + throw MappingException::mappingFileNotFound($className, $filename); + } + + throw MappingException::mappingFileNotFound($className, substr($className, strrpos($className, '\\') + 1).$this->_fileExtension); + } +} diff --git a/lib/Doctrine/ORM/Mapping/Driver/SimplifiedYamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/SimplifiedYamlDriver.php new file mode 100644 index 000000000..679ee85cc --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/Driver/SimplifiedYamlDriver.php @@ -0,0 +1,182 @@ +. +*/ + +namespace Doctrine\ORM\Mapping\Driver; + +use Doctrine\ORM\Mapping\MappingException; + +/** + * YamlDriver that additionally looks for mapping information in a global file. + * + * @author Fabien Potencier + * @author Benjamin Eberlei + * @license MIT + */ +class SimplifiedYamlDriver extends YamlDriver +{ + protected $_prefixes = array(); + protected $_globalBasename; + protected $_classCache; + protected $_fileExtension = '.orm.yml'; + + public function __construct($prefixes) + { + $this->addNamespacePrefixes($prefixes); + } + + public function setGlobalBasename($file) + { + $this->_globalBasename = $file; + } + + public function getGlobalBasename() + { + return $this->_globalBasename; + } + + public function addNamespacePrefixes($prefixes) + { + $this->_prefixes = array_merge($this->_prefixes, $prefixes); + $this->addPaths(array_flip($prefixes)); + } + + public function addNamespacePrefix($prefix, $path) + { + $this->_prefixes[$path] = $prefix; + } + + + public function getNamespacePrefixes() + { + return $this->_prefixes; + } + + public function isTransient($className) + { + if (null === $this->_classCache) { + $this->initialize(); + } + + // The mapping is defined in the global mapping file + if (isset($this->_classCache[$className])) { + return false; + } + + try { + $this->_findMappingFile($className); + + return false; + } catch (MappingException $e) { + return true; + } + } + + public function getAllClassNames() + { + if (null === $this->_classCache) { + $this->initialize(); + } + + $classes = array(); + + if ($this->_paths) { + foreach ((array) $this->_paths as $path) { + if (!is_dir($path)) { + throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($path), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) { + $fileName = $file->getBasename($this->_fileExtension); + + if ($fileName == $file->getBasename() || $fileName == $this->_globalBasename) { + continue; + } + + // NOTE: All files found here means classes are not transient! + if (isset($this->_prefixes[$path])) { + $classes[] = $this->_prefixes[$path].'\\'.str_replace('.', '\\', $fileName); + } else { + $classes[] = str_replace('.', '\\', $fileName); + } + } + } + } + + return array_merge($classes, array_keys($this->_classCache)); + } + + public function getElement($className) + { + if (null === $this->_classCache) { + $this->initialize(); + } + + if (!isset($this->_classCache[$className])) { + $this->_classCache[$className] = parent::getElement($className); + } + + return $this->_classCache[$className]; + } + + protected function initialize() + { + $this->_classCache = array(); + if (null !== $this->_globalBasename) { + foreach ($this->_paths as $path) { + if (is_file($file = $path.'/'.$this->_globalBasename.$this->_fileExtension)) { + $this->_classCache = array_merge($this->_classCache, $this->_loadMappingFile($file)); + } + } + } + } + + protected function _findMappingFile($className) + { + $defaultFileName = str_replace('\\', '.', $className).$this->_fileExtension; + foreach ($this->_paths as $path) { + if (!isset($this->_prefixes[$path])) { + if (is_file($path.DIRECTORY_SEPARATOR.$defaultFileName)) { + return $path.DIRECTORY_SEPARATOR.$defaultFileName; + } + + continue; + } + + $prefix = $this->_prefixes[$path]; + + if (0 !== strpos($className, $prefix.'\\')) { + continue; + } + + $filename = $path.'/'.strtr(substr($className, strlen($prefix)+1), '\\', '.').$this->_fileExtension; + if (is_file($filename)) { + return $filename; + } + + throw MappingException::mappingFileNotFound($className, $filename); + } + + throw MappingException::mappingFileNotFound($className, substr($className, strrpos($className, '\\') + 1).$this->_fileExtension); + } +} diff --git a/lib/Doctrine/ORM/Mapping/MappingException.php b/lib/Doctrine/ORM/Mapping/MappingException.php index 97fbf94f6..fcdd5c575 100644 --- a/lib/Doctrine/ORM/Mapping/MappingException.php +++ b/lib/Doctrine/ORM/Mapping/MappingException.php @@ -288,7 +288,7 @@ class MappingException extends \Doctrine\ORM\ORMException public static function mappedClassNotPartOfDiscriminatorMap($className, $rootClassName) { return new self( - "Entity '" . $className . "' has to be part of the descriminator map of '" . $rootClassName . "' " . + "Entity '" . $className . "' has to be part of the discriminator map of '" . $rootClassName . "' " . "to be properly mapped in the inheritance hierachy. Alternatively you can make '".$className."' an abstract class " . "to avoid this exception from occuring." ); diff --git a/lib/Doctrine/ORM/ORMException.php b/lib/Doctrine/ORM/ORMException.php index aa9657552..c156893c5 100644 --- a/lib/Doctrine/ORM/ORMException.php +++ b/lib/Doctrine/ORM/ORMException.php @@ -34,7 +34,7 @@ class ORMException extends Exception return new self("It's a requirement to specify a Metadata Driver and pass it ". "to Doctrine\ORM\Configuration::setMetadataDriverImpl()."); } - + public static function entityMissingForeignAssignedId($entity, $relatedEntity) { return new self( @@ -46,15 +46,14 @@ class ORMException extends Exception ); } - public static function entityMissingAssignedId($entity) + public static function entityMissingAssignedIdForField($entity, $field) { - return new self("Entity of type " . get_class($entity) . " is missing an assigned ID. " . + return new self("Entity of type " . get_class($entity) . " is missing an assigned ID for field '" . $field . "'. " . "The identifier generation strategy for this entity requires the ID field to be populated before ". - "EntityManager#persist() is called. If you want automatically generated identifiers instead " . + "EntityManager#persist() is called. If you want automatically generated identifiers instead " . "you need to adjust the metadata mapping accordingly." ); } - public static function unrecognizedField($field) { return new self("Unrecognized field: $field"); @@ -130,8 +129,8 @@ class ORMException extends Exception "Unknown Entity namespace alias '$entityNamespaceAlias'." ); } - - public static function invalidEntityRepository($className) + + public static function invalidEntityRepository($className) { return new self("Invalid repository class '".$className."'. ". "it must be a Doctrine\ORM\EntityRepository."); diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index a093f9a09..0379ccf56 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -580,12 +580,13 @@ class BasicEntityPersister * @param $assoc The association that connects the entity to load to another entity, if any. * @param array $hints Hints for entity creation. * @param int $lockMode + * @param int $limit Limit number of results * @return object The loaded and managed entity instance or NULL if the entity can not be found. * @todo Check identity map? loadById method? Try to guess whether $criteria is the id? */ - public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0) + public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0, $limit = null) { - $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, $lockMode); + $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, $lockMode, $limit); list($params, $types) = $this->expandParameters($criteria); $stmt = $this->_conn->executeQuery($sql, $params, $types); @@ -747,6 +748,7 @@ class BasicEntityPersister * * @param array $assoc * @param Doctrine\DBAL\Statement $stmt + * * @return array */ private function loadArrayFromStatement($assoc, $stmt) @@ -771,6 +773,8 @@ class BasicEntityPersister * @param array $assoc * @param Doctrine\DBAL\Statement $stmt * @param PersistentCollection $coll + * + * @return array */ private function loadCollectionFromStatement($assoc, $stmt, $coll) { @@ -784,7 +788,8 @@ class BasicEntityPersister } $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); - $hydrator->hydrateAll($stmt, $rsm, $hints); + + return $hydrator->hydrateAll($stmt, $rsm, $hints); } /** @@ -819,7 +824,7 @@ class BasicEntityPersister if (isset($sourceClass->associationMappings[$field])) { $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value); - $value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]]; + $value = $value[$this->_em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]]; } $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $value; @@ -843,7 +848,7 @@ class BasicEntityPersister if (isset($sourceClass->associationMappings[$field])) { $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value); - $value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]]; + $value = $value[$this->_em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]]; } $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $value; @@ -1056,10 +1061,10 @@ class BasicEntityPersister if ($columnList) $columnList .= ', '; $columnAlias = $srcColumn . $this->_sqlAliasCounter++; - $columnList .= $this->_getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias) ) - . '.' . $srcColumn . ' AS ' . $columnAlias; $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); - $this->_rsm->addMetaResult($alias, $this->_platform->getSQLResultCasing($columnAlias), $srcColumn, isset($assoc['id']) && $assoc['id'] === true); + $columnList .= $this->_getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias) ) + . '.' . $srcColumn . ' AS ' . $resultColumnName; + $this->_rsm->addMetaResult($alias, $resultColumnName, $srcColumn, isset($assoc['id']) && $assoc['id'] === true); } } @@ -1179,6 +1184,7 @@ class BasicEntityPersister $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) . '.' . $class->getQuotedColumnName($field, $this->_platform); $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++); + $this->_rsm->addFieldResult($alias, $columnAlias, $field); return $sql . ' AS ' . $columnAlias; @@ -1228,7 +1234,9 @@ class BasicEntityPersister $sql = 'SELECT 1 ' . $this->_platform->appendLockHint($this->getLockTablesSql(), $lockMode) . ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ' . $lockSql; + list($params, $types) = $this->expandParameters($criteria); + $stmt = $this->_conn->executeQuery($sql, $params, $types); } @@ -1320,7 +1328,8 @@ class BasicEntityPersister public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) { $stmt = $this->getOneToManyStatement($assoc, $sourceEntity); - $this->loadCollectionFromStatement($assoc, $stmt, $coll); + + return $this->loadCollectionFromStatement($assoc, $stmt, $coll); } /** @@ -1347,7 +1356,7 @@ class BasicEntityPersister if (isset($sourceClass->associationMappings[$field])) { $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value); - $value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]]; + $value = $value[$this->_em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]]; } $criteria[$tableAlias . "." . $targetKeyColumn] = $value; @@ -1478,6 +1487,7 @@ class BasicEntityPersister public function exists($entity, array $extraConditions = array()) { $criteria = $this->_class->getIdentifierValues($entity); + if ($extraConditions) { $criteria = array_merge($criteria, $extraConditions); } @@ -1485,7 +1495,9 @@ class BasicEntityPersister $sql = 'SELECT 1' . ' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($this->_class->name) . ' WHERE ' . $this->_getSelectConditionSQL($criteria); - - return (bool) $this->_conn->fetchColumn($sql, array_values($criteria)); + + list($params, $types) = $this->expandParameters($criteria); + + return (bool) $this->_conn->fetchColumn($sql, $params); } } diff --git a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php index c9ab27c69..b39d3bf75 100644 --- a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php +++ b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php @@ -41,6 +41,10 @@ class SingleTablePersister extends AbstractEntityInheritancePersister /** {@inheritdoc} */ protected function _getSelectColumnListSQL() { + if ($this->_selectColumnListSql !== null) { + return $this->_selectColumnListSql; + } + $columnList = parent::_getSelectColumnListSQL(); // Append discriminator column @@ -81,7 +85,8 @@ class SingleTablePersister extends AbstractEntityInheritancePersister } } - return $columnList; + $this->_selectColumnListSql = $columnList; + return $this->_selectColumnListSql; } /** {@inheritdoc} */ diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index 8d62ca7ab..490c3a119 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -209,6 +209,11 @@ class ProxyFactory $methods .= $parameterString . ')'; $methods .= "\n" . ' {' . "\n"; + if ($this->isShortIdentifierGetter($method, $class)) { + $methods .= ' if ($this->__isInitialized__ === false) {' . "\n"; + $methods .= ' return $this->_identifier["' . lcfirst(substr($method->getName(), 3)) . '"];' . "\n"; + $methods .= ' }' . "\n"; + } $methods .= ' $this->__load();' . "\n"; $methods .= ' return parent::' . $method->getName() . '(' . $argumentString . ');'; $methods .= "\n" . ' }' . "\n"; @@ -218,6 +223,23 @@ class ProxyFactory return $methods; } + /** + * @param ReflectionMethod $method + * @param ClassMetadata $class + * @return bool + */ + private function isShortIdentifierGetter($method, $class) + { + $identifier = lcfirst(substr($method->getName(), 3)); + return ( + $method->getNumberOfParameters() == 0 && + substr($method->getName(), 0, 3) == "get" && + in_array($identifier, $class->identifier, true) && + $class->hasField($identifier) && + (($method->getEndLine() - $method->getStartLine()) <= 4) + ); + } + /** * Generates the code for the __sleep method for a proxy class. * @@ -288,7 +310,7 @@ class extends \ implements \Doctrine\ORM\Proxy\Proxy unset($this->_entityPersister, $this->_identifier); } } - + public function __sleep() diff --git a/lib/Doctrine/ORM/Query/AST/SelectExpression.php b/lib/Doctrine/ORM/Query/AST/SelectExpression.php index f1793f0c4..e3c917200 100644 --- a/lib/Doctrine/ORM/Query/AST/SelectExpression.php +++ b/lib/Doctrine/ORM/Query/AST/SelectExpression.php @@ -23,7 +23,7 @@ namespace Doctrine\ORM\Query\AST; /** * SelectExpression ::= IdentificationVariable ["." "*"] | StateFieldPathExpression | - * (AggregateExpression | "(" Subselect ")") [["AS"] FieldAliasIdentificationVariable] + * (AggregateExpression | "(" Subselect ")") [["AS"] ["HIDDEN"] FieldAliasIdentificationVariable] * * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @link www.doctrine-project.org @@ -37,11 +37,13 @@ class SelectExpression extends Node { public $expression; public $fieldIdentificationVariable; + public $hiddenAliasResultVariable; - public function __construct($expression, $fieldIdentificationVariable) + public function __construct($expression, $fieldIdentificationVariable, $hiddenAliasResultVariable = false) { $this->expression = $expression; $this->fieldIdentificationVariable = $fieldIdentificationVariable; + $this->hiddenAliasResultVariable = $hiddenAliasResultVariable; } public function dispatch($sqlWalker) diff --git a/lib/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php b/lib/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php index 311c818ce..a6c22cecd 100644 --- a/lib/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php +++ b/lib/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php @@ -98,7 +98,7 @@ class MultiTableDeleteExecutor extends AbstractSqlExecutor } $this->_createTempTableSql = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' (' . $platform->getColumnDeclarationListSQL($columnDefinitions) . ')'; - $this->_dropTempTableSql = 'DROP TABLE ' . $tempTable; + $this->_dropTempTableSql = $platform->getDropTemporaryTableSQL($tempTable); } /** diff --git a/lib/Doctrine/ORM/Query/Exec/MultiTableUpdateExecutor.php b/lib/Doctrine/ORM/Query/Exec/MultiTableUpdateExecutor.php index b45abe83b..2607f582b 100644 --- a/lib/Doctrine/ORM/Query/Exec/MultiTableUpdateExecutor.php +++ b/lib/Doctrine/ORM/Query/Exec/MultiTableUpdateExecutor.php @@ -137,7 +137,7 @@ class MultiTableUpdateExecutor extends AbstractSqlExecutor $this->_createTempTableSql = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' (' . $platform->getColumnDeclarationListSQL($columnDefinitions) . ')'; - $this->_dropTempTableSql = 'DROP TABLE ' . $tempTable; + $this->_dropTempTableSql = $platform->getDropTemporaryTableSQL($tempTable); } /** diff --git a/lib/Doctrine/ORM/Query/Expr.php b/lib/Doctrine/ORM/Query/Expr.php index 246f92717..6ba6a5d73 100644 --- a/lib/Doctrine/ORM/Query/Expr.php +++ b/lib/Doctrine/ORM/Query/Expr.php @@ -40,10 +40,12 @@ class Expr * * [php] * // (u.type = ?1) AND (u.role = ?2) - * $expr->andX('u.type = ?1', 'u.role = ?2')); + * $expr->andX($expr->eq('u.type', ':1'), $expr->eq('u.role', ':2')); * - * @param mixed $x Optional clause. Defaults = null, but requires - * at least one defined when converting to string. + * @param Doctrine\ORM\Query\Expr\Comparison | + * Doctrine\ORM\Query\Expr\Func | + * Doctrine\ORM\Query\Expr\Orx + * $x Optional clause. Defaults = null, but requires at least one defined when converting to string. * @return Expr\Andx */ public function andX($x = null) diff --git a/lib/Doctrine/ORM/Query/Expr/Base.php b/lib/Doctrine/ORM/Query/Expr/Base.php index abe7e54be..beac2bb9c 100644 --- a/lib/Doctrine/ORM/Query/Expr/Base.php +++ b/lib/Doctrine/ORM/Query/Expr/Base.php @@ -45,17 +45,19 @@ abstract class Base { $this->addMultiple($args); } - + public function addMultiple($args = array()) { foreach ((array) $args as $arg) { $this->add($arg); } + + return $this; } public function add($arg) { - if ( $arg !== null || ($arg instanceof self && $arg->count() > 0)) { + if ( $arg !== null ) { // If we decide to keep Expr\Base instances, we can use this check if ( ! is_string($arg)) { $class = get_class($arg); @@ -67,6 +69,8 @@ abstract class Base $this->_parts[] = $arg; } + + return $this; } public function count() @@ -79,7 +83,7 @@ abstract class Base if ($this->count() == 1) { return (string) $this->_parts[0]; } - + return $this->_preSeparator . implode($this->_separator, $this->_parts) . $this->_postSeparator; } -} \ No newline at end of file +} diff --git a/lib/Doctrine/ORM/Query/Expr/From.php b/lib/Doctrine/ORM/Query/Expr/From.php index 6646e1de2..4c0c8175a 100644 --- a/lib/Doctrine/ORM/Query/Expr/From.php +++ b/lib/Doctrine/ORM/Query/Expr/From.php @@ -34,27 +34,55 @@ namespace Doctrine\ORM\Query\Expr; */ class From { + /** + * @var string + */ private $_from; + + /** + * @var string + */ private $_alias; - - public function __construct($from, $alias) + + /** + * @var string + */ + private $_indexBy; + + /** + * @param string $from The class name. + * @param string $alias The alias of the class. + * @param string $indexBy The index for the from. + */ + public function __construct($from, $alias, $indexBy = null) { - $this->_from = $from; - $this->_alias = $alias; + $this->_from = $from; + $this->_alias = $alias; + $this->_indexBy = $indexBy; } - + + /** + * @return string + */ public function getFrom() { return $this->_from; } + /** + * @return string + */ public function getAlias() { return $this->_alias; } + /** + * @return string + */ public function __toString() { - return $this->_from . ' ' . $this->_alias; + return $this->_from . ' ' . $this->_alias . + ($this->_indexBy ? ' INDEX BY ' . $this->_indexBy : ''); } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/Lexer.php b/lib/Doctrine/ORM/Query/Lexer.php index 9d20689cf..00fcffd8c 100644 --- a/lib/Doctrine/ORM/Query/Lexer.php +++ b/lib/Doctrine/ORM/Query/Lexer.php @@ -76,39 +76,40 @@ class Lexer extends \Doctrine\Common\Lexer const T_FROM = 122; const T_GROUP = 123; const T_HAVING = 124; - const T_IN = 125; - const T_INDEX = 126; - const T_INNER = 127; - const T_INSTANCE = 128; - const T_IS = 129; - const T_JOIN = 130; - const T_LEADING = 131; - const T_LEFT = 132; - const T_LIKE = 133; - const T_MAX = 134; - const T_MEMBER = 135; - const T_MIN = 136; - const T_NOT = 137; - const T_NULL = 138; - const T_NULLIF = 139; - const T_OF = 140; - const T_OR = 141; - const T_ORDER = 142; - const T_OUTER = 143; - const T_SELECT = 144; - const T_SET = 145; - const T_SIZE = 146; - const T_SOME = 147; - const T_SUM = 148; - const T_THEN = 149; - const T_TRAILING = 150; - const T_TRUE = 151; - const T_UPDATE = 152; - const T_WHEN = 153; - const T_WHERE = 154; - const T_WITH = 155; - const T_PARTIAL = 156; - const T_MOD = 157; + const T_HIDDEN = 125; + const T_IN = 126; + const T_INDEX = 127; + const T_INNER = 128; + const T_INSTANCE = 129; + const T_IS = 130; + const T_JOIN = 131; + const T_LEADING = 132; + const T_LEFT = 133; + const T_LIKE = 134; + const T_MAX = 135; + const T_MEMBER = 136; + const T_MIN = 137; + const T_NOT = 138; + const T_NULL = 139; + const T_NULLIF = 140; + const T_OF = 141; + const T_OR = 142; + const T_ORDER = 143; + const T_OUTER = 144; + const T_SELECT = 145; + const T_SET = 146; + const T_SIZE = 147; + const T_SOME = 148; + const T_SUM = 149; + const T_THEN = 150; + const T_TRAILING = 151; + const T_TRUE = 152; + const T_UPDATE = 153; + const T_WHEN = 154; + const T_WHERE = 155; + const T_WITH = 156; + const T_PARTIAL = 157; + const T_MOD = 158; /** * Creates a new query scanner object. diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index d5b57a00d..9fc30bb8c 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -1328,18 +1328,18 @@ class Parser // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression $glimpse = $this->_lexer->glimpse(); - if ($glimpse['type'] != Lexer::T_DOT) { - $token = $this->_lexer->lookahead; - $identVariable = $this->IdentificationVariable(); + if ($glimpse['type'] == Lexer::T_DOT) { + return $this->SingleValuedPathExpression(); + } + + $token = $this->_lexer->lookahead; + $identVariable = $this->IdentificationVariable(); - if (!isset($this->_queryComponents[$identVariable])) { - $this->semanticalError('Cannot group by undefined identification variable.'); - } - - return $identVariable; + if (!isset($this->_queryComponents[$identVariable])) { + $this->semanticalError('Cannot group by undefined identification variable.'); } - return $this->SingleValuedPathExpression(); + return $identVariable; } /** @@ -1354,12 +1354,9 @@ class Parser // We need to check if we are in a ResultVariable or StateFieldPathExpression $glimpse = $this->_lexer->glimpse(); - if ($glimpse['type'] != Lexer::T_DOT) { - $token = $this->_lexer->lookahead; - $expr = $this->ResultVariable(); - } else { - $expr = $this->SingleValuedPathExpression(); - } + $expr = ($glimpse['type'] != Lexer::T_DOT) + ? $this->ResultVariable() + : $this->SingleValuedPathExpression(); $item = new AST\OrderByItem($expr); @@ -1831,7 +1828,7 @@ class Parser /** * SelectExpression ::= * IdentificationVariable | StateFieldPathExpression | - * (AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable] + * (AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] ["HIDDEN"] AliasResultVariable] * * @return Doctrine\ORM\Query\AST\SelectExpression */ @@ -1839,6 +1836,7 @@ class Parser { $expression = null; $identVariable = null; + $hiddenAliasResultVariable = false; $fieldAliasIdentificationVariable = null; $peek = $this->_lexer->glimpse(); @@ -1900,6 +1898,12 @@ class Parser if ($this->_lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); } + + if ($this->_lexer->isNextToken(Lexer::T_HIDDEN)) { + $this->match(Lexer::T_HIDDEN); + + $hiddenAliasResultVariable = true; + } if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) { $token = $this->_lexer->lookahead; @@ -1914,10 +1918,12 @@ class Parser } } - $expr = new AST\SelectExpression($expression, $fieldAliasIdentificationVariable); - if (!$supportsAlias) { + $expr = new AST\SelectExpression($expression, $fieldAliasIdentificationVariable, $hiddenAliasResultVariable); + + if ( ! $supportsAlias) { $this->_identVariableExpressions[$identVariable] = $expr; } + return $expr; } diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 0f1c79542..188350197 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -975,8 +975,9 @@ class SqlWalker implements TreeWalker */ public function walkSelectExpression($selectExpression) { - $sql = ''; - $expr = $selectExpression->expression; + $sql = ''; + $expr = $selectExpression->expression; + $hidden = $selectExpression->hiddenAliasResultVariable; if ($expr instanceof AST\PathExpression) { if ($expr->type !== AST\PathExpression::TYPE_STATE_FIELD) { @@ -1006,7 +1007,10 @@ class SqlWalker implements TreeWalker $columnAlias = $this->getSQLColumnAlias($columnName); $sql .= $sqlTableAlias . '.' . $columnName . ' AS ' . $columnAlias; $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - $this->_rsm->addScalarResult($columnAlias, $resultAlias); + + if ( ! $hidden) { + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } } else if ($expr instanceof AST\AggregateExpression) { if ( ! $selectExpression->fieldIdentificationVariable) { $resultAlias = $this->_scalarResultCounter++; @@ -1019,7 +1023,10 @@ class SqlWalker implements TreeWalker $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - $this->_rsm->addScalarResult($columnAlias, $resultAlias); + + if ( ! $hidden) { + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } } else if ($expr instanceof AST\Subselect) { if ( ! $selectExpression->fieldIdentificationVariable) { $resultAlias = $this->_scalarResultCounter++; @@ -1032,7 +1039,10 @@ class SqlWalker implements TreeWalker $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - $this->_rsm->addScalarResult($columnAlias, $resultAlias); + + if ( ! $hidden) { + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } } else if ($expr instanceof AST\Functions\FunctionNode) { if ( ! $selectExpression->fieldIdentificationVariable) { $resultAlias = $this->_scalarResultCounter++; @@ -1045,7 +1055,10 @@ class SqlWalker implements TreeWalker $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - $this->_rsm->addScalarResult($columnAlias, $resultAlias); + + if ( ! $hidden) { + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } } else if ( $expr instanceof AST\SimpleArithmeticExpression || $expr instanceof AST\ArithmeticTerm || @@ -1070,7 +1083,10 @@ class SqlWalker implements TreeWalker $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - $this->_rsm->addScalarResult($columnAlias, $resultAlias); + + if ( ! $hidden) { + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } } else if ( $expr instanceof AST\NullIfExpression || $expr instanceof AST\CoalesceExpression || @@ -1090,7 +1106,10 @@ class SqlWalker implements TreeWalker $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - $this->_rsm->addScalarResult($columnAlias, $resultAlias); + + if ( ! $hidden) { + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } } else { // IdentificationVariable or PartialObjectExpression if ($expr instanceof AST\PartialObjectExpression) { @@ -1107,8 +1126,9 @@ class SqlWalker implements TreeWalker if ( ! isset($this->_selectedClasses[$dqlAlias])) { $this->_selectedClasses[$dqlAlias] = $class; } - + $beginning = true; + // Select all fields from the queried class foreach ($class->fieldMappings as $fieldName => $mapping) { if ($partialFieldSet && !in_array($fieldName, $partialFieldSet)) { @@ -1127,6 +1147,7 @@ class SqlWalker implements TreeWalker . ' AS ' . $columnAlias; $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); + $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); } @@ -1138,6 +1159,7 @@ class SqlWalker implements TreeWalker foreach ($class->subClasses as $subClassName) { $subClass = $this->_em->getClassMetadata($subClassName); $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); + foreach ($subClass->fieldMappings as $fieldName => $mapping) { if (isset($mapping['inherited']) || $partialFieldSet && !in_array($fieldName, $partialFieldSet)) { continue; @@ -1159,8 +1181,10 @@ class SqlWalker implements TreeWalker if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE && ! isset($assoc['inherited'])) { foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { if ($beginning) $beginning = false; else $sql .= ', '; + $columnAlias = $this->getSQLColumnAlias($srcColumn); $sql .= $sqlTableAlias . '.' . $srcColumn . ' AS ' . $columnAlias; + $this->_rsm->addMetaResult($dqlAlias, $this->_platform->getSQLResultCasing($columnAlias), $srcColumn); } } @@ -1826,12 +1850,16 @@ class SqlWalker implements TreeWalker switch ($literal->type) { case AST\Literal::STRING: return $this->_conn->quote($literal->value); + case AST\Literal::BOOLEAN: $bool = strtolower($literal->value) == 'true' ? true : false; $boolVal = $this->_conn->getDatabasePlatform()->convertBooleans($bool); - return is_string($boolVal) ? $this->_conn->quote($boolVal) : $boolVal; + + return $boolVal; + case AST\Literal::NUMERIC: return $literal->value; + default: throw QueryException::invalidLiteral($literal); } diff --git a/lib/Doctrine/ORM/QueryBuilder.php b/lib/Doctrine/ORM/QueryBuilder.php index 9b2829408..938a429ae 100644 --- a/lib/Doctrine/ORM/QueryBuilder.php +++ b/lib/Doctrine/ORM/QueryBuilder.php @@ -595,11 +595,12 @@ class QueryBuilder * * @param string $from The class name. * @param string $alias The alias of the class. + * @param string $indexBy The index for the from. * @return QueryBuilder This QueryBuilder instance. */ - public function from($from, $alias) + public function from($from, $alias, $indexBy = null) { - return $this->add('from', new Expr\From($from, $alias), true); + return $this->add('from', new Expr\From($from, $alias, $indexBy), true); } /** diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/MetadataCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/MetadataCommand.php index ef7b02e3c..13243a0f7 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/MetadataCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/MetadataCommand.php @@ -21,7 +21,8 @@ namespace Doctrine\ORM\Tools\Console\Command\ClearCache; use Symfony\Component\Console\Input\InputArgument, Symfony\Component\Console\Input\InputOption, - Symfony\Component\Console; + Symfony\Component\Console, + Doctrine\Common\Cache; /** * Command to clear the metadata cache of the various cache drivers. @@ -83,6 +84,10 @@ EOT if ( ! $cacheDriver) { throw new \InvalidArgumentException('No Metadata cache driver is configured on given EntityManager.'); } + + if ($cacheDriver instanceof Cache\ApcCache) { + throw new \LogicException("Cannot clear APC Cache from Console, its shared in the Webserver memory and not accessible from the CLI."); + } $output->write('Clearing ALL Metadata cache entries' . PHP_EOL); diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php index becb78a90..6451d4ad7 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php @@ -21,7 +21,8 @@ namespace Doctrine\ORM\Tools\Console\Command\ClearCache; use Symfony\Component\Console\Input\InputArgument, Symfony\Component\Console\Input\InputOption, - Symfony\Component\Console; + Symfony\Component\Console, + Doctrine\Common\Cache; /** * Command to clear the query cache of the various cache drivers. @@ -83,6 +84,10 @@ EOT if ( ! $cacheDriver) { throw new \InvalidArgumentException('No Query cache driver is configured on given EntityManager.'); } + + if ($cacheDriver instanceof Cache\ApcCache) { + throw new \LogicException("Cannot clear APC Cache from Console, its shared in the Webserver memory and not accessible from the CLI."); + } $output->write('Clearing ALL Query cache entries' . PHP_EOL); diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/ResultCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/ResultCommand.php index 5db7fb296..1dfacb056 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/ResultCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/ResultCommand.php @@ -21,7 +21,8 @@ namespace Doctrine\ORM\Tools\Console\Command\ClearCache; use Symfony\Component\Console\Input\InputArgument, Symfony\Component\Console\Input\InputOption, - Symfony\Component\Console; + Symfony\Component\Console, + Doctrine\Common\Cache; /** * Command to clear the result cache of the various cache drivers. @@ -78,13 +79,17 @@ EOT protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output) { $em = $this->getHelper('em')->getEntityManager(); - $cacheDriver = $em->getConfiguration()->getQueryCacheImpl(); + $cacheDriver = $em->getConfiguration()->getResultCacheImpl(); if ( ! $cacheDriver) { throw new \InvalidArgumentException('No Result cache driver is configured on given EntityManager.'); } + + if ($cacheDriver instanceof Cache\ApcCache) { + throw new \LogicException("Cannot clear APC Cache from Console, its shared in the Webserver memory and not accessible from the CLI."); + } - $output->write('Clearing ALL Query cache entries' . PHP_EOL); + $output->write('Clearing ALL Result cache entries' . PHP_EOL); $result = $cacheDriver->deleteAll(); $message = ($result) ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.'; diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index e53505f98..62efbac85 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -115,10 +115,12 @@ public function () * * * @param $ + * @return */ public function ($) { $this-> = $; +return $this; }'; private static $_addMethodTemplate = @@ -150,7 +152,7 @@ public function () public function __construct() { - if (version_compare(\Doctrine\Common\Version::VERSION, '3.0.0-DEV', '>=')) { + if (version_compare(\Doctrine\Common\Version::VERSION, '2.2.0-DEV', '>=')) { $this->_annotationsPrefix = 'ORM\\'; } } @@ -302,9 +304,6 @@ public function () */ public function setAnnotationPrefix($prefix) { - if (version_compare(\Doctrine\Common\Version::VERSION, '2.2.0-DEV', '>=')) { - return; - } $this->_annotationsPrefix = $prefix; } @@ -399,14 +398,17 @@ public function () } $collections = array(); + foreach ($metadata->associationMappings AS $mapping) { if ($mapping['type'] & ClassMetadataInfo::TO_MANY) { $collections[] = '$this->'.$mapping['fieldName'].' = new \Doctrine\Common\Collections\ArrayCollection();'; } } + if ($collections) { return $this->_prefixCodeWithSpaces(str_replace("", implode("\n", $collections), self::$_constructorMethodTemplate)); } + return ''; } @@ -734,7 +736,8 @@ public function () '' => $variableType, '' => Inflector::camelize($fieldName), '' => $methodName, - '' => $fieldName + '' => $fieldName, + '' => $this->_getClassName($metadata) ); $method = str_replace( @@ -788,7 +791,7 @@ public function () } if (isset($joinColumn['onDelete'])) { - $joinColumnAnnot[] = 'onDelete=' . ($joinColumn['onDelete'] ? 'true' : 'false'); + $joinColumnAnnot[] = 'onDelete="' . ($joinColumn['onDelete'] . '"'); } if (isset($joinColumn['columnDefinition'])) { diff --git a/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php b/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php index f9668c78f..01309475b 100644 --- a/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php +++ b/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php @@ -139,6 +139,9 @@ class XmlExporter extends AbstractExporter if (isset($field['columnName'])) { $idXml->addAttribute('column', $field['columnName']); } + if (isset($field['associationKey']) && $field['associationKey']) { + $idXml->addAttribute('association-key', 'true'); + } if ($idGeneratorType = $this->_getIdGeneratorTypeString($metadata->generatorType)) { $generatorXml = $idXml->addChild('generator'); $generatorXml->addAttribute('strategy', $idGeneratorType); diff --git a/lib/Doctrine/ORM/Tools/SchemaValidator.php b/lib/Doctrine/ORM/Tools/SchemaValidator.php index 8acaa0b54..a7f8e3a1c 100644 --- a/lib/Doctrine/ORM/Tools/SchemaValidator.php +++ b/lib/Doctrine/ORM/Tools/SchemaValidator.php @@ -152,6 +152,17 @@ class SchemaValidator "has to be a primary key column."; } } + + if (count($targetClass->identifier) != count($assoc['joinTable']['inverseJoinColumns'])) { + $ce[] = "The inverse join columns of the many-to-many table '" . $assoc['joinTable']['name'] . "' " . + "have to match to ALL identifier columns of the target entity '". $targetClass->name . "'"; + } + + if (count($class->identifier) != count($assoc['joinTable']['joinColumns'])) { + $ce[] = "The join columns of the many-to-many table '" . $assoc['joinTable']['name'] . "' " . + "have to match to ALL identifier columns of the source entity '". $class->name . "'"; + } + } else if ($assoc['type'] & ClassMetadataInfo::TO_ONE) { foreach ($assoc['joinColumns'] AS $joinColumn) { $targetClass = $cmf->getMetadataFor($assoc['targetEntity']); @@ -167,6 +178,11 @@ class SchemaValidator "has to be a primary key column."; } } + + if (count($class->identifier) != count($assoc['joinColumns'])) { + $ce[] = "The join columns of the association '" . $assoc['fieldName'] . "' " . + "have to match to ALL identifier columns of the source entity '". $class->name . "'"; + } } } diff --git a/lib/Doctrine/ORM/Tools/Setup.php b/lib/Doctrine/ORM/Tools/Setup.php index 39b6465a3..b4a6a2c7f 100644 --- a/lib/Doctrine/ORM/Tools/Setup.php +++ b/lib/Doctrine/ORM/Tools/Setup.php @@ -97,7 +97,7 @@ class Setup require_once $directory . "/Doctrine/Common/ClassLoader.php"; } - $loader = new ClassLoader("Doctrine"); + $loader = new ClassLoader("Doctrine", $directory); $loader->register(); $loader = new ClassLoader("Symfony\Component", $directory . "/Doctrine"); diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index d5965d2d4..4366250dd 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -215,8 +215,13 @@ class UnitOfWork implements PropertyChangedListener * @var array */ private $orphanRemovals = array(); - - //private $_readOnlyObjects = array(); + + /** + * Read-Only objects are never evaluated + * + * @var array + */ + private $readOnlyObjects = array(); /** * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested. @@ -248,12 +253,18 @@ class UnitOfWork implements PropertyChangedListener * 3) All collection deletions * 4) All collection updates * 5) All entity deletions - * + * + * @param object $entity + * @return void */ - public function commit() + public function commit($entity = null) { // Compute changes done since last commit. - $this->computeChangeSets(); + if ($entity === null) { + $this->computeChangeSets(); + } else { + $this->computeSingleEntityChangeSet($entity); + } if ( ! ($this->entityInsertions || $this->entityDeletions || @@ -323,12 +334,17 @@ class UnitOfWork implements PropertyChangedListener $conn->rollback(); throw $e; } - + // Take new snapshots from visited collections foreach ($this->visitedCollections as $coll) { $coll->takeSnapshot(); } + // Raise postFlush + if ($this->evm->hasListeners(Events::postFlush)) { + $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->em)); + } + // Clear up $this->entityInsertions = $this->entityUpdates = @@ -341,6 +357,61 @@ class UnitOfWork implements PropertyChangedListener $this->scheduledForDirtyCheck = $this->orphanRemovals = array(); } + + /** + * Compute the changesets of all entities scheduled for insertion + * + * @return void + */ + private function computeScheduleInsertsChangeSets() + { + foreach ($this->entityInsertions as $entity) { + $class = $this->em->getClassMetadata(get_class($entity)); + $this->computeChangeSet($class, $entity); + } + } + + /** + * Only flush the given entity according to a ruleset that keeps the UoW consistent. + * + * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well! + * 2. Read Only entities are skipped. + * 3. Proxies are skipped. + * 4. Only if entity is properly managed. + * + * @param object $entity + * @return void + */ + private function computeSingleEntityChangeSet($entity) + { + if ( ! $this->isInIdentityMap($entity) ) { + throw new \InvalidArgumentException("Entity has to be managed for single computation " . self::objToStr($entity)); + } + + $class = $this->em->getClassMetadata(get_class($entity)); + + if ($class->isChangeTrackingDeferredImplicit()) { + $this->persist($entity); + } + + // Compute changes for INSERTed entities first. This must always happen even in this case. + $this->computeScheduleInsertsChangeSets(); + + if ( $class->isReadOnly ) { + return; + } + + // Ignore uninitialized proxy objects + if ($entity instanceof Proxy && ! $entity->__isInitialized__) { + return; + } + + // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here. + $oid = spl_object_hash($entity); + if ( ! isset($this->entityInsertions[$oid]) && isset($this->entityStates[$oid])) { + $this->computeChangeSet($class, $entity); + } + } /** * Executes any extra updates that have been scheduled. @@ -393,6 +464,8 @@ class UnitOfWork implements PropertyChangedListener * If a PersistentCollection has been de-referenced in a fully MANAGED entity, * then this collection is marked for deletion. * + * @ignore + * @internal Don't call from the outside. * @param ClassMetadata $class The class descriptor of the entity. * @param object $entity The entity for which to compute the changes. */ @@ -403,6 +476,11 @@ class UnitOfWork implements PropertyChangedListener } $oid = spl_object_hash($entity); + + if (isset($this->readOnlyObjects[$oid])) { + return; + } + $actualData = array(); foreach ($class->reflFields as $name => $refProp) { $value = $refProp->getValue($entity); @@ -458,7 +536,15 @@ class UnitOfWork implements PropertyChangedListener $changeSet = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid])) ? $this->entityChangeSets[$oid] : array(); foreach ($actualData as $propName => $actualValue) { - $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null; + if (isset($originalData[$propName])) { + $orgValue = $originalData[$propName]; + } else if (array_key_exists($propName, $originalData)) { + $orgValue = null; + } else { + // skip field, its a partially omitted one! + continue; + } + if (isset($class->associationMappings[$propName])) { $assoc = $class->associationMappings[$propName]; if ($assoc['type'] & ClassMetadata::TO_ONE && $orgValue !== $actualValue) { @@ -470,8 +556,9 @@ class UnitOfWork implements PropertyChangedListener } } else if ($orgValue instanceof PersistentCollection && $orgValue !== $actualValue) { // A PersistentCollection was de-referenced, so delete it. - if ( ! in_array($orgValue, $this->collectionDeletions, true)) { - $this->collectionDeletions[] = $orgValue; + $coid = spl_object_hash($orgValue); + if ( ! isset($this->collectionDeletions[$coid]) ) { + $this->collectionDeletions[$coid] = $orgValue; $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored. } } @@ -505,10 +592,7 @@ class UnitOfWork implements PropertyChangedListener public function computeChangeSets() { // Compute changes for INSERTed entities first. This must always happen. - foreach ($this->entityInsertions as $entity) { - $class = $this->em->getClassMetadata(get_class($entity)); - $this->computeChangeSet($class, $entity); - } + $this->computeScheduleInsertsChangeSets(); // Compute changes for other MANAGED entities. Change tracking policies take effect here. foreach ($this->identityMap as $className => $entities) { @@ -528,7 +612,7 @@ class UnitOfWork implements PropertyChangedListener foreach ($entitiesToProcess as $entity) { // Ignore uninitialized proxy objects - if (/* $entity is readOnly || */ $entity instanceof Proxy && ! $entity->__isInitialized__) { + if ($entity instanceof Proxy && ! $entity->__isInitialized__) { continue; } // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here. @@ -549,10 +633,11 @@ class UnitOfWork implements PropertyChangedListener private function computeAssociationChanges($assoc, $value) { if ($value instanceof PersistentCollection && $value->isDirty()) { + $coid = spl_object_hash($value); if ($assoc['isOwningSide']) { - $this->collectionUpdates[] = $value; + $this->collectionUpdates[$coid] = $value; } - $this->visitedCollections[] = $value; + $this->visitedCollections[$coid] = $value; } // Look through the entities, and in any of their associations, for transient (new) @@ -635,7 +720,7 @@ class UnitOfWork implements PropertyChangedListener * @param object $entity The entity for which to (re)calculate the change set. * @throws InvalidArgumentException If the passed entity is not MANAGED. */ - public function recomputeSingleEntityChangeSet($class, $entity) + public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity) { $oid = spl_object_hash($entity); @@ -853,6 +938,7 @@ class UnitOfWork implements PropertyChangedListener $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); if ( ! $calc->hasClass($targetClass->name)) { $calc->addClass($targetClass); + $newNodes[] = $targetClass; } $calc->addDependency($targetClass, $class); // If the target class has mapped subclasses, @@ -1387,6 +1473,10 @@ class UnitOfWork implements PropertyChangedListener if ($this->getEntityState($entity, self::STATE_DETACHED) == self::STATE_MANAGED) { $managedCopy = $entity; } else { + if ($entity instanceof Proxy && ! $entity->__isInitialized__) { + $entity->__load(); + } + // Try to look the entity up in the identity map. $id = $class->getIdentifierValues($entity); @@ -1448,11 +1538,17 @@ class UnitOfWork implements PropertyChangedListener if ($this->getEntityState($other, self::STATE_DETACHED) == self::STATE_MANAGED) { $prop->setValue($managedCopy, $other); } else { + $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']); - $id = $targetClass->getIdentifierValues($other); - $proxy = $this->em->getProxyFactory()->getProxy($assoc2['targetEntity'], $id); - $prop->setValue($managedCopy, $proxy); - $this->registerManaged($proxy, $id, array()); + $relatedId = $targetClass->getIdentifierValues($other); + + if ($targetClass->subClasses) { + $entity = $this->em->find($targetClass->name, $relatedId); + } else { + $proxy = $this->em->getProxyFactory()->getProxy($assoc2['targetEntity'], $relatedId); + $prop->setValue($managedCopy, $proxy); + $this->registerManaged($proxy, $relatedId, array()); + } } } } else { @@ -1534,8 +1630,9 @@ class UnitOfWork implements PropertyChangedListener * * @param object $entity * @param array $visited + * @param boolean $noCascade if true, don't cascade detach operation */ - private function doDetach($entity, array &$visited) + private function doDetach($entity, array &$visited, $noCascade = false) { $oid = spl_object_hash($entity); if (isset($visited[$oid])) { @@ -1557,8 +1654,10 @@ class UnitOfWork implements PropertyChangedListener case self::STATE_DETACHED: return; } - - $this->cascadeDetach($entity, $visited); + + if (!$noCascade) { + $this->cascadeDetach($entity, $visited); + } } /** @@ -1809,28 +1908,41 @@ class UnitOfWork implements PropertyChangedListener /** * Clears the UnitOfWork. + * + * @param string $entityName if given, only entities of this type will get detached */ - public function clear() + public function clear($entityName = null) { - $this->identityMap = - $this->entityIdentifiers = - $this->originalEntityData = - $this->entityChangeSets = - $this->entityStates = - $this->scheduledForDirtyCheck = - $this->entityInsertions = - $this->entityUpdates = - $this->entityDeletions = - $this->collectionDeletions = - $this->collectionUpdates = - $this->extraUpdates = - $this->orphanRemovals = array(); - if ($this->commitOrderCalculator !== null) { - $this->commitOrderCalculator->clear(); + if ($entityName === null) { + $this->identityMap = + $this->entityIdentifiers = + $this->originalEntityData = + $this->entityChangeSets = + $this->entityStates = + $this->scheduledForDirtyCheck = + $this->entityInsertions = + $this->entityUpdates = + $this->entityDeletions = + $this->collectionDeletions = + $this->collectionUpdates = + $this->extraUpdates = + $this->orphanRemovals = array(); + if ($this->commitOrderCalculator !== null) { + $this->commitOrderCalculator->clear(); + } + } else { + $visited = array(); + foreach ($this->identityMap as $className => $entities) { + if ($className === $entityName) { + foreach ($entities as $entity) { + $this->doDetach($entity, $visited, true); + } + } + } } if ($this->evm->hasListeners(Events::onClear)) { - $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em)); + $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em, $entityName)); } } @@ -1858,12 +1970,12 @@ class UnitOfWork implements PropertyChangedListener { //TODO: if $coll is already scheduled for recreation ... what to do? // Just remove $coll from the scheduled recreations? - $this->collectionDeletions[] = $coll; + $this->collectionDeletions[spl_object_hash($coll)] = $coll; } public function isCollectionScheduledForDeletion(PersistentCollection $coll) { - return in_array($coll, $this->collectionsDeletions, true); + return isset( $this->collectionsDeletions[spl_object_hash($coll)] ); } /** @@ -2403,4 +2515,37 @@ class UnitOfWork implements PropertyChangedListener { return method_exists($obj, '__toString') ? (string)$obj : get_class($obj).'@'.spl_object_hash($obj); } + + /** + * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit(). + * + * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information + * on this object that might be necessary to perform a correct udpate. + * + * @throws \InvalidArgumentException + * @param $object + * @return void + */ + public function markReadOnly($object) + { + if ( ! is_object($object) || ! $this->isInIdentityMap($object)) { + throw new InvalidArgumentException("Managed entity required"); + } + $this->readOnlyObjects[spl_object_hash($object)] = true; + } + + /** + * Is this entity read only? + * + * @throws \InvalidArgumentException + * @param $object + * @return void + */ + public function isReadOnly($object) + { + if ( ! is_object($object) ) { + throw new InvalidArgumentException("Managed entity required"); + } + return isset($this->readOnlyObjects[spl_object_hash($object)]); + } } diff --git a/lib/vendor/doctrine-common b/lib/vendor/doctrine-common index ef431a148..b2fd909b4 160000 --- a/lib/vendor/doctrine-common +++ b/lib/vendor/doctrine-common @@ -1 +1 @@ -Subproject commit ef431a14852d7e8f2d0ea789487509ab266e5ce2 +Subproject commit b2fd909b4b5476df01744c9d34c7a23723a687b6 diff --git a/tests/Doctrine/Tests/Models/CMS/CmsUser.php b/tests/Doctrine/Tests/Models/CMS/CmsUser.php index f02d0441b..3296fffc8 100644 --- a/tests/Doctrine/Tests/Models/CMS/CmsUser.php +++ b/tests/Doctrine/Tests/Models/CMS/CmsUser.php @@ -19,7 +19,7 @@ class CmsUser */ public $id; /** - * @Column(type="string", length=50) + * @Column(type="string", length=50, nullable=true) */ public $status; /** @@ -35,7 +35,7 @@ class CmsUser */ public $phonenumbers; /** - * @OneToMany(targetEntity="CmsArticle", mappedBy="user") + * @OneToMany(targetEntity="CmsArticle", mappedBy="user", cascade={"detach"}) */ public $articles; /** @@ -48,7 +48,7 @@ class CmsUser */ public $email; /** - * @ManyToMany(targetEntity="CmsGroup", inversedBy="users", cascade={"persist", "merge"}) + * @ManyToMany(targetEntity="CmsGroup", inversedBy="users", cascade={"persist", "merge", "detach"}) * @JoinTable(name="cms_users_groups", * joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")}, * inverseJoinColumns={@JoinColumn(name="group_id", referencedColumnName="id")} diff --git a/tests/Doctrine/Tests/Models/Company/CompanyContract.php b/tests/Doctrine/Tests/Models/Company/CompanyContract.php index fd534f403..655d4fccb 100644 --- a/tests/Doctrine/Tests/Models/Company/CompanyContract.php +++ b/tests/Doctrine/Tests/Models/Company/CompanyContract.php @@ -7,7 +7,11 @@ namespace Doctrine\Tests\Models\Company; * @Table(name="company_contracts") * @InheritanceType("SINGLE_TABLE") * @DiscriminatorColumn(name="discr", type="string") - * @DiscriminatorMap({"fix" = "CompanyFixContract", "flexible" = "CompanyFlexContract", "flexultra" = "CompanyFlexUltraContract"}) + * @DiscriminatorMap({ + * "fix" = "CompanyFixContract", + * "flexible" = "CompanyFlexContract", + * "flexultra" = "CompanyFlexUltraContract" + * }) */ abstract class CompanyContract { diff --git a/tests/Doctrine/Tests/ORM/Event/EntityEventDelegateeTest.php b/tests/Doctrine/Tests/ORM/Event/EntityEventDelegateeTest.php new file mode 100644 index 000000000..5c4db0101 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Event/EntityEventDelegateeTest.php @@ -0,0 +1,91 @@ +delegator = new \Doctrine\ORM\Event\EntityEventDelegator(); + } + + public function testGetSubscribedEventsWhenEmpty() + { + $this->assertEquals(array(), $this->delegator->getSubscribedEvents()); + } + + public function testAddListener() + { + $this->delegator->addEventListener('postLoad', 'stdClass', new DelegateeEventListener()); + $this->assertEquals(array('postLoad'), $this->delegator->getSubscribedEvents()); + } + + public function testAddSubscriber() + { + $this->delegator->addEventSubscriber(new DelegateeEventListener(), 'stdClass'); + $this->assertEquals(array('postLoad'), $this->delegator->getSubscribedEvents()); + } + + public function testAddListenerAfterFrozenThrowsException() + { + $this->delegator->getSubscribedEvents(); // freezes + + $this->setExpectedException("LogicException", "Cannot add event listeners aft"); + $this->delegator->addEventListener('postLoad', 'stdClass', new DelegateeEventListener()); + } + + public function testDelegateEvent() + { + $delegatee = new DelegateeEventListener(); + $this->delegator->addEventListener('postLoad', 'stdClass', $delegatee); + + $event = new \Doctrine\ORM\Event\LifecycleEventArgs(new \stdClass(), $this->_getTestEntityManager()); + $this->delegator->postLoad($event); + $this->delegator->postLoad($event); + + $this->assertEquals(2, $delegatee->postLoad); + } + + public function testDelegatePickEntity() + { + $delegatee = new DelegateeEventListener(); + $this->delegator->addEventListener('postLoad', 'stdClass', $delegatee); + + $event1 = new \Doctrine\ORM\Event\LifecycleEventArgs(new \stdClass(), $this->_getTestEntityManager()); + $event2 = new \Doctrine\ORM\Event\LifecycleEventArgs(new \Doctrine\Tests\Models\CMS\CmsUser(), $this->_getTestEntityManager()); + $this->delegator->postLoad($event1); + $this->delegator->postLoad($event2); + + $this->assertEquals(1, $delegatee->postLoad); + } +} + +class DelegateeEventListener implements \Doctrine\Common\EventSubscriber +{ + public $postLoad = 0; + + public function postLoad($args) + { + $this->postLoad++; + } + + /** + * Returns an array of events this subscriber wants to listen to. + * + * @return array + */ + function getSubscribedEvents() + { + return array('postLoad'); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index bed8de33d..feef41c76 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -863,7 +863,6 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase public function testGetPartialReferenceToUpdateObjectWithoutLoadingIt() { - //$this->_em->getConnection()->getConfiguration()->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger); $user = new CmsUser(); $user->username = "beberlei"; $user->name = "Benjamin E."; @@ -882,7 +881,7 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->flush(); $this->_em->clear(); - $this->assertEquals('Stephan', $this->_em->find(get_class($user), $userId)->name); + $this->assertEquals('Benjamin E.', $this->_em->find(get_class($user), $userId)->name); } public function testMergePersistsNewEntities() @@ -981,4 +980,223 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertTrue($article->user->__isInitialized__, "...but its initialized!"); $this->assertEquals($qc+2, $this->getCurrentQueryCount()); } + + /** + * @group DDC-1278 + */ + public function testClearWithEntityName() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + + $address = new CmsAddress(); + $address->city = "Springfield"; + $address->zip = "12354"; + $address->country = "Germany"; + $address->street = "Foo Street"; + $address->user = $user; + $user->address = $address; + + $article1 = new CmsArticle(); + $article1->topic = 'Foo'; + $article1->text = 'Foo Text'; + + $article2 = new CmsArticle(); + $article2->topic = 'Bar'; + $article2->text = 'Bar Text'; + + $user->addArticle($article1); + $user->addArticle($article2); + + $this->_em->persist($article1); + $this->_em->persist($article2); + $this->_em->persist($address); + $this->_em->persist($user); + $this->_em->flush(); + + $unitOfWork = $this->_em->getUnitOfWork(); + + $this->_em->clear('Doctrine\Tests\Models\CMS\CmsUser'); + + $this->assertEquals(\Doctrine\ORM\UnitOfWork::STATE_DETACHED, $unitOfWork->getEntityState($user)); + + $this->assertEquals(\Doctrine\ORM\UnitOfWork::STATE_MANAGED, $unitOfWork->getEntityState($address)); + $this->assertEquals(\Doctrine\ORM\UnitOfWork::STATE_MANAGED, $unitOfWork->getEntityState($article1)); + $this->assertEquals(\Doctrine\ORM\UnitOfWork::STATE_MANAGED, $unitOfWork->getEntityState($article2)); + + $this->_em->clear(); + + $this->assertEquals(\Doctrine\ORM\UnitOfWork::STATE_DETACHED, $unitOfWork->getEntityState($address)); + } + + /** + * @group DDC-720 + */ + public function testFlushSingleManagedEntity() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + + $this->_em->persist($user); + $this->_em->flush(); + + $user->status = 'administrator'; + $this->_em->flush($user); + $this->_em->clear(); + + $user = $this->_em->find(get_class($user), $user->id); + $this->assertEquals('administrator', $user->status); + } + + /** + * @group DDC-720 + */ + public function testFlushSingleUnmanagedEntity() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + + $this->setExpectedException('InvalidArgumentException', 'Entity has to be managed for single computation'); + $this->_em->flush($user); + } + + /** + * @group DDC-720 + */ + public function testFlushSingleAndNewEntity() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + + $this->_em->persist($user); + $this->_em->flush(); + + $otherUser = new CmsUser; + $otherUser->name = 'Dominik2'; + $otherUser->username = 'domnikl2'; + $otherUser->status = 'developer'; + + $user->status = 'administrator'; + + $this->_em->persist($otherUser); + $this->_em->flush($user); + + $this->assertTrue($this->_em->contains($otherUser), "Other user is contained in EntityManager"); + $this->assertTrue($otherUser->id > 0, "other user has an id"); + } + + /** + * @group DDC-720 + */ + public function testFlushAndCascadePersist() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + + $this->_em->persist($user); + $this->_em->flush(); + + $address = new CmsAddress(); + $address->city = "Springfield"; + $address->zip = "12354"; + $address->country = "Germany"; + $address->street = "Foo Street"; + $address->user = $user; + $user->address = $address; + + $this->_em->flush($user); + + $this->assertTrue($this->_em->contains($address), "Other user is contained in EntityManager"); + $this->assertTrue($address->id > 0, "other user has an id"); + } + + /** + * @group DDC-720 + */ + public function testFlushSingleAndNoCascade() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + + $this->_em->persist($user); + $this->_em->flush(); + + $article1 = new CmsArticle(); + $article1->topic = 'Foo'; + $article1->text = 'Foo Text'; + $article1->author = $user; + $user->articles[] = $article1; + + $this->setExpectedException('InvalidArgumentException', "A new entity was found through the relationship 'Doctrine\Tests\Models\CMS\CmsUser#articles'"); + $this->_em->flush($user); + } + + /** + * @group DDC-720 + */ + public function testProxyIsIgnored() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + + $this->_em->persist($user); + $this->_em->flush(); + $this->_em->clear(); + + $user = $this->_em->getReference(get_class($user), $user->id); + + $otherUser = new CmsUser; + $otherUser->name = 'Dominik2'; + $otherUser->username = 'domnikl2'; + $otherUser->status = 'developer'; + + $this->_em->persist($otherUser); + $this->_em->flush($user); + + $this->assertTrue($this->_em->contains($otherUser), "Other user is contained in EntityManager"); + $this->assertTrue($otherUser->id > 0, "other user has an id"); + } + + /** + * @group DDC-720 + */ + public function testFlushSingleSaveOnlySingle() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + $this->_em->persist($user); + + $user2 = new CmsUser; + $user2->name = 'Dominik'; + $user2->username = 'domnikl2'; + $user2->status = 'developer'; + $this->_em->persist($user2); + + $this->_em->flush(); + + $user->status = 'admin'; + $user2->status = 'admin'; + + $this->_em->flush($user); + $this->_em->clear(); + + $user2 = $this->_em->find(get_class($user2), $user2->id); + $this->assertEquals('developer', $user2->status); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php b/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php index 37ce0b6ea..999d52db8 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php @@ -98,7 +98,7 @@ class DatabaseDriverTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertArrayHasKey('CmsUsers', $metadatas, 'CmsUsers entity was not detected.'); $this->assertArrayHasKey('CmsGroups', $metadatas, 'CmsGroups entity was not detected.'); - $this->assertEquals(1, count($metadatas['CmsUsers']->associationMappings)); + $this->assertEquals(2, count($metadatas['CmsUsers']->associationMappings)); $this->assertArrayHasKey('group', $metadatas['CmsUsers']->associationMappings); $this->assertEquals(1, count($metadatas['CmsGroups']->associationMappings)); $this->assertArrayHasKey('user', $metadatas['CmsGroups']->associationMappings); diff --git a/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php b/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php index 3da6bc09b..2e17ccf12 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php @@ -54,7 +54,30 @@ class DefaultValuesTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals($userId, $a2->getUser()->getId()); $this->assertEquals('Poweruser', $a2->getUser()->type); } - + + /** + * @group DDC-1386 + */ + public function testGetPartialReferenceWithDefaultValueNotEvalutedInFlush() + { + $user = new DefaultValueUser; + $user->name = 'romanb'; + $user->type = 'Normaluser'; + + $this->_em->persist($user); + $this->_em->flush(); + $this->_em->clear(); + + $user = $this->_em->getPartialReference('Doctrine\Tests\ORM\Functional\DefaultValueUser', $user->id); + $this->assertTrue($this->_em->getUnitOfWork()->isReadOnly($user)); + + $this->_em->flush(); + $this->_em->clear(); + + $user = $this->_em->find('Doctrine\Tests\ORM\Functional\DefaultValueUser', $user->id); + + $this->assertEquals('Normaluser', $user->type); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index c5216fb94..4f8e11420 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -38,18 +38,25 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $user2->status = 'dev'; $this->_em->persist($user2); + $user3 = new CmsUser; + $user3->name = 'Benjamin'; + $user3->username = 'beberlei'; + $user3->status = null; + $this->_em->persist($user3); + $this->_em->flush(); - + $user1Id = $user->getId(); - + unset($user); unset($user2); - + unset($user3); + $this->_em->clear(); return $user1Id; } - + public function loadAssociatedFixture() { $address = new CmsAddress(); @@ -189,7 +196,7 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); $users = $repos->findAll(); - $this->assertEquals(2, count($users)); + $this->assertEquals(3, count($users)); } public function testFindByAlias() @@ -202,7 +209,7 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $repos = $this->_em->getRepository('CMS:CmsUser'); $users = $repos->findAll(); - $this->assertEquals(2, count($users)); + $this->assertEquals(3, count($users)); } /** @@ -284,10 +291,11 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase public function testFindMagicCallByNullValue() { $this->loadFixture(); + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); - $this->setExpectedException('Doctrine\ORM\ORMException'); $users = $repos->findByStatus(null); + $this->assertEquals(1, count($users)); } /** @@ -389,7 +397,7 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-1087 */ - public function testIsNullCriteria() + public function testIsNullCriteriaDoesNotGenerateAParameter() { $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); $users = $repos->findBy(array('status' => null, 'username' => 'romanb')); @@ -399,6 +407,16 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(array('romanb'), $params); } + public function testIsNullCriteria() + { + $this->loadFixture(); + + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + + $users = $repos->findBy(array('status' => null)); + $this->assertEquals(1, count($users)); + } + /** * @group DDC-1094 */ @@ -411,7 +429,7 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $users1 = $repos->findBy(array(), null, 1, 0); $users2 = $repos->findBy(array(), null, 1, 1); - $this->assertEquals(2, count($repos->findBy(array()))); + $this->assertEquals(3, count($repos->findBy(array()))); $this->assertEquals(1, count($users1)); $this->assertEquals(1, count($users2)); $this->assertNotSame($users1[0], $users2[0]); @@ -428,10 +446,10 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $usersAsc = $repos->findBy(array(), array("username" => "ASC")); $usersDesc = $repos->findBy(array(), array("username" => "DESC")); - $this->assertEquals(2, count($usersAsc), "Pre-condition: only two users in fixture"); - $this->assertEquals(2, count($usersDesc), "Pre-condition: only two users in fixture"); - $this->assertSame($usersAsc[0], $usersDesc[1]); - $this->assertSame($usersAsc[1], $usersDesc[0]); + $this->assertEquals(3, count($usersAsc), "Pre-condition: only three users in fixture"); + $this->assertEquals(3, count($usersDesc), "Pre-condition: only three users in fixture"); + $this->assertSame($usersAsc[0], $usersDesc[2]); + $this->assertSame($usersAsc[2], $usersDesc[0]); } diff --git a/tests/Doctrine/Tests/ORM/Functional/LifecycleCallbackTest.php b/tests/Doctrine/Tests/ORM/Functional/LifecycleCallbackTest.php index 1ddd7a8ff..2d71541d2 100644 --- a/tests/Doctrine/Tests/ORM/Functional/LifecycleCallbackTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/LifecycleCallbackTest.php @@ -78,7 +78,7 @@ class LifecycleCallbackTest extends \Doctrine\Tests\OrmFunctionalTestCase $reference = $this->_em->getReference('Doctrine\Tests\ORM\Functional\LifecycleCallbackTestEntity', $id); $this->assertFalse($reference->postLoadCallbackInvoked); - $reference->getId(); // trigger proxy load + $reference->getValue(); // trigger proxy load $this->assertTrue($reference->postLoadCallbackInvoked); } @@ -210,6 +210,10 @@ class LifecycleCallbackTestEntity return $this->id; } + public function getValue() { + return $this->value; + } + /** @PrePersist */ public function doStuffOnPrePersist() { $this->prePersistCallbackInvoked = true; @@ -274,4 +278,4 @@ class LifecycleListenerPreUpdate { $eventArgs->setNewValue('name', 'Bob'); } -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/ORM/Functional/PostFlushEventTest.php b/tests/Doctrine/Tests/ORM/Functional/PostFlushEventTest.php new file mode 100644 index 000000000..819887196 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/PostFlushEventTest.php @@ -0,0 +1,95 @@ + + */ +class PostFlushEventTest extends \Doctrine\Tests\OrmFunctionalTestCase +{ + /** + * @var PostFlushListener + */ + private $listener; + + protected function setUp() + { + $this->useModelSet('cms'); + parent::setUp(); + $this->listener = new PostFlushListener(); + $evm = $this->_em->getEventManager(); + $evm->addEventListener(Events::postFlush, $this->listener); + } + + public function testListenerShouldBeNotified() + { + $this->_em->persist($this->createNewValidUser()); + $this->_em->flush(); + $this->assertTrue($this->listener->wasNotified); + } + + public function testListenerShouldNotBeNotifiedWhenFlushThrowsException() + { + $user = new CmsUser(); + $user->username = 'dfreudenberger'; + $this->_em->persist($user); + $exceptionRaised = false; + + try { + $this->_em->flush(); + } catch (\Exception $ex) { + $exceptionRaised = true; + } + + $this->assertTrue($exceptionRaised); + $this->assertFalse($this->listener->wasNotified); + } + + public function testListenerShouldReceiveEntityManagerThroughArgs() + { + $this->_em->persist($this->createNewValidUser()); + $this->_em->flush(); + $receivedEm = $this->listener->receivedArgs->getEntityManager(); + $this->assertSame($this->_em, $receivedEm); + } + + /** + * @return CmsUser + */ + private function createNewValidUser() + { + $user = new CmsUser(); + $user->username = 'dfreudenberger'; + $user->name = 'Daniel Freudenberger'; + return $user; + } +} + +class PostFlushListener +{ + /** + * @var bool + */ + public $wasNotified = false; + + /** + * @var PostFlushEventArgs + */ + public $receivedArgs; + + /** + * @param PostFlushEventArgs $args + */ + public function postFlush(PostFlushEventArgs $args) + { + $this->wasNotified = true; + $this->receivedArgs = $args; + } +} + + diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryCacheTest.php index eb7d662d6..13a774b76 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryCacheTest.php @@ -104,7 +104,7 @@ class QueryCacheTest extends \Doctrine\Tests\OrmFunctionalTestCase $query = $this->_em->createQuery('select ux from Doctrine\Tests\Models\CMS\CmsUser ux'); - $cache = $this->getMock('Doctrine\Common\Cache\ArrayCache', array('doFetch', 'doSave')); + $cache = $this->getMock('Doctrine\Common\Cache\ArrayCache', array('doFetch', 'doSave', 'doGetStats')); $cache->expects($this->at(0)) ->method('doFetch') ->with($this->isType('string')) @@ -135,7 +135,7 @@ class QueryCacheTest extends \Doctrine\Tests\OrmFunctionalTestCase ->will($this->returnValue($sqlExecMock)); $cache = $this->getMock('Doctrine\Common\Cache\CacheProvider', - array('doFetch', 'doContains', 'doSave', 'doDelete', 'doFlush')); + array('doFetch', 'doContains', 'doSave', 'doDelete', 'doFlush', 'doGetStats')); $cache->expects($this->once()) ->method('doFetch') ->with($this->isType('string')) diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php index 993ce1642..596d929f6 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php @@ -533,4 +533,34 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(2, count($users)); } + + public function testQueryWithHiddenAsSelectExpression() + { + $userA = new CmsUser; + $userA->name = 'Benjamin'; + $userA->username = 'beberlei'; + $userA->status = 'developer'; + $this->_em->persist($userA); + + $userB = new CmsUser; + $userB->name = 'Roman'; + $userB->username = 'romanb'; + $userB->status = 'developer'; + $this->_em->persist($userB); + + $userC = new CmsUser; + $userC->name = 'Jonathan'; + $userC->username = 'jwage'; + $userC->status = 'developer'; + $this->_em->persist($userC); + + $this->_em->flush(); + $this->_em->clear(); + + $query = $this->_em->createQuery("SELECT u, (SELECT COUNT(u2.id) FROM Doctrine\Tests\Models\CMS\CmsUser u2) AS HIDDEN total FROM Doctrine\Tests\Models\CMS\CmsUser u"); + $users = $query->execute(); + + $this->assertEquals(3, count($users)); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $users[0]); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php index 8ecb389af..b89f3d04e 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php @@ -147,4 +147,28 @@ class ReferenceProxyTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertTrue($entity->wakeUp, "Loading the proxy should call __wakeup()."); } + + public function testDoNotInitializeProxyOnGettingTheIdentifier() + { + $id = $this->createProduct(); + + /* @var $entity Doctrine\Tests\Models\ECommerce\ECommerceProduct */ + $entity = $this->_em->getReference('Doctrine\Tests\Models\ECommerce\ECommerceProduct' , $id); + + $this->assertFalse($entity->__isInitialized__, "Pre-Condition: Object is unitialized proxy."); + $this->assertEquals($id, $entity->getId()); + $this->assertFalse($entity->__isInitialized__, "Getting the identifier doesn't initialize the proxy."); + } + + public function testInitializeProxyOnGettingSomethingOtherThanTheIdentifier() + { + $id = $this->createProduct(); + + /* @var $entity Doctrine\Tests\Models\ECommerce\ECommerceProduct */ + $entity = $this->_em->getReference('Doctrine\Tests\Models\ECommerce\ECommerceProduct' , $id); + + $this->assertFalse($entity->__isInitialized__, "Pre-Condition: Object is unitialized proxy."); + $this->assertEquals('Doctrine Cookbook', $entity->getName()); + $this->assertTrue($entity->__isInitialized__, "Getting something other than the identifier initializes the proxy."); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/SchemaTool/MySqlSchemaToolTest.php b/tests/Doctrine/Tests/ORM/Functional/SchemaTool/MySqlSchemaToolTest.php index 50e8a95c9..be671bf3d 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SchemaTool/MySqlSchemaToolTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SchemaTool/MySqlSchemaToolTest.php @@ -27,15 +27,16 @@ class MySqlSchemaToolTest extends \Doctrine\Tests\OrmFunctionalTestCase $tool = new SchemaTool($this->_em); $sql = $tool->getCreateSchemaSql($classes); $this->assertEquals("CREATE TABLE cms_addresses (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, country VARCHAR(50) NOT NULL, zip VARCHAR(50) NOT NULL, city VARCHAR(50) NOT NULL, UNIQUE INDEX UNIQ_ACAC157BA76ED395 (user_id), PRIMARY KEY(id)) ENGINE = InnoDB", $sql[0]); - $this->assertEquals("CREATE TABLE cms_users (id INT AUTO_INCREMENT NOT NULL, status VARCHAR(50) NOT NULL, username VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_3AF03EC5F85E0677 (username), PRIMARY KEY(id)) ENGINE = InnoDB", $sql[1]); + $this->assertEquals("CREATE TABLE cms_users (id INT AUTO_INCREMENT NOT NULL, email_id INT DEFAULT NULL, status VARCHAR(50) DEFAULT NULL, username VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_3AF03EC5F85E0677 (username), UNIQUE INDEX UNIQ_3AF03EC5A832C1C9 (email_id), PRIMARY KEY(id)) ENGINE = InnoDB", $sql[1]); $this->assertEquals("CREATE TABLE cms_users_groups (user_id INT NOT NULL, group_id INT NOT NULL, INDEX IDX_7EA9409AA76ED395 (user_id), INDEX IDX_7EA9409AFE54D947 (group_id), PRIMARY KEY(user_id, group_id)) ENGINE = InnoDB", $sql[2]); $this->assertEquals("CREATE TABLE cms_phonenumbers (phonenumber VARCHAR(50) NOT NULL, user_id INT DEFAULT NULL, INDEX IDX_F21F790FA76ED395 (user_id), PRIMARY KEY(phonenumber)) ENGINE = InnoDB", $sql[3]); - $this->assertEquals("ALTER TABLE cms_addresses ADD FOREIGN KEY (user_id) REFERENCES cms_users(id)", $sql[4]); - $this->assertEquals("ALTER TABLE cms_users_groups ADD FOREIGN KEY (user_id) REFERENCES cms_users(id)", $sql[5]); - $this->assertEquals("ALTER TABLE cms_users_groups ADD FOREIGN KEY (group_id) REFERENCES cms_groups(id)", $sql[6]); - $this->assertEquals("ALTER TABLE cms_phonenumbers ADD FOREIGN KEY (user_id) REFERENCES cms_users(id)", $sql[7]); - - $this->assertEquals(8, count($sql)); + $this->assertEquals("ALTER TABLE cms_addresses ADD CONSTRAINT FK_ACAC157BA76ED395 FOREIGN KEY (user_id) REFERENCES cms_users (id)", $sql[4]); + $this->assertEquals("ALTER TABLE cms_users ADD CONSTRAINT FK_3AF03EC5A832C1C9 FOREIGN KEY (email_id) REFERENCES cms_emails (id)", $sql[5]); + $this->assertEquals("ALTER TABLE cms_users_groups ADD CONSTRAINT FK_7EA9409AA76ED395 FOREIGN KEY (user_id) REFERENCES cms_users (id)", $sql[6]); + $this->assertEquals("ALTER TABLE cms_users_groups ADD CONSTRAINT FK_7EA9409AFE54D947 FOREIGN KEY (group_id) REFERENCES cms_groups (id)", $sql[7]); + $this->assertEquals("ALTER TABLE cms_phonenumbers ADD CONSTRAINT FK_F21F790FA76ED395 FOREIGN KEY (user_id) REFERENCES cms_users (id)", $sql[8]); + + $this->assertEquals(9, count($sql)); } public function testGetCreateSchemaSql2() @@ -63,4 +64,4 @@ class MySqlSchemaToolTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(1, count($sql)); $this->assertEquals("CREATE TABLE boolean_model (id INT AUTO_INCREMENT NOT NULL, booleanField TINYINT(1) NOT NULL, PRIMARY KEY(id)) ENGINE = InnoDB", $sql[0]); } -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1238Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1238Test.php index 93a89818e..467577a43 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1238Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1238Test.php @@ -52,6 +52,8 @@ class DDC1238Test extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->flush(); $this->_em->clear(); + // force proxy load, getId() doesn't work anymore + $user->getName(); $userId = $user->getId(); $this->_em->clear(); @@ -60,6 +62,8 @@ class DDC1238Test extends \Doctrine\Tests\OrmFunctionalTestCase $user2 = $this->_em->getReference(__NAMESPACE__ . '\\DDC1238User', $userId); + // force proxy load, getId() doesn't work anymore + $user->getName(); $this->assertNull($user->getId(), "Now this is null, we already have a user instance of that type"); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1335Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1335Test.php new file mode 100644 index 000000000..e12ee9ab7 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1335Test.php @@ -0,0 +1,214 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1135User'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1135Phone'), + )); + $this->loadFixture(); + } catch(\Exception $e) { + } + } + + + public function testDql() + { + $dql = 'SELECT u FROM ' . __NAMESPACE__ . '\DDC1135User u INDEX BY u.id'; + $query = $this->_em->createQuery($dql); + $result = $query->getResult(); + + $this->assertEquals(sizeof($result), 3); + $this->assertArrayHasKey(1, $result); + $this->assertArrayHasKey(2, $result); + $this->assertArrayHasKey(3, $result); + + $dql = 'SELECT u, p FROM '.__NAMESPACE__ . '\DDC1135User u INDEX BY u.email INNER JOIN u.phones p INDEX BY p.id'; + $query = $this->_em->createQuery($dql); + $result = $query->getResult(); + + $this->assertEquals(sizeof($result), 3); + $this->assertArrayHasKey('foo@foo.com', $result); + $this->assertArrayHasKey('bar@bar.com', $result); + $this->assertArrayHasKey('foobar@foobar.com', $result); + + $this->assertEquals(sizeof($result['foo@foo.com']->phones), 3); + $this->assertEquals(sizeof($result['bar@bar.com']->phones), 3); + $this->assertEquals(sizeof($result['foobar@foobar.com']->phones), 3); + + $this->assertArrayHasKey(1, $result['foo@foo.com']->phones->toArray()); + $this->assertArrayHasKey(2, $result['foo@foo.com']->phones->toArray()); + $this->assertArrayHasKey(3, $result['foo@foo.com']->phones->toArray()); + + $this->assertArrayHasKey(4, $result['bar@bar.com']->phones->toArray()); + $this->assertArrayHasKey(5, $result['bar@bar.com']->phones->toArray()); + $this->assertArrayHasKey(6, $result['bar@bar.com']->phones->toArray()); + + $this->assertArrayHasKey(7, $result['foobar@foobar.com']->phones->toArray()); + $this->assertArrayHasKey(8, $result['foobar@foobar.com']->phones->toArray()); + $this->assertArrayHasKey(9, $result['foobar@foobar.com']->phones->toArray()); + } + + public function testTicket() + { + $builder = $this->_em->createQueryBuilder(); + $builder->select('u')->from(__NAMESPACE__ . '\DDC1135User', 'u', 'u.id'); + + $dql = $builder->getQuery()->getDQL(); + $result = $builder->getQuery()->getResult(); + + $this->assertEquals(sizeof($result), 3); + $this->assertArrayHasKey(1, $result); + $this->assertArrayHasKey(2, $result); + $this->assertArrayHasKey(3, $result); + $this->assertEquals('SELECT u FROM ' . __NAMESPACE__ . '\DDC1135User u INDEX BY u.id', $dql); + } + + public function testIndexByUnique() + { + $builder = $this->_em->createQueryBuilder(); + $builder->select('u')->from(__NAMESPACE__ . '\DDC1135User', 'u', 'u.email'); + + $dql = $builder->getQuery()->getDQL(); + $result = $builder->getQuery()->getResult(); + + $this->assertEquals(sizeof($result), 3); + $this->assertArrayHasKey('foo@foo.com', $result); + $this->assertArrayHasKey('bar@bar.com', $result); + $this->assertArrayHasKey('foobar@foobar.com', $result); + $this->assertEquals('SELECT u FROM ' . __NAMESPACE__ . '\DDC1135User u INDEX BY u.email', $dql); + } + + public function testIndexWithJoin() + { + $builder = $this->_em->createQueryBuilder(); + $builder->select('u','p') + ->from(__NAMESPACE__ . '\DDC1135User', 'u', 'u.email') + ->join('u.phones', 'p', null, null, 'p.id'); + + $dql = $builder->getQuery()->getDQL(); + $result = $builder->getQuery()->getResult(); + + $this->assertEquals(sizeof($result), 3); + $this->assertArrayHasKey('foo@foo.com', $result); + $this->assertArrayHasKey('bar@bar.com', $result); + $this->assertArrayHasKey('foobar@foobar.com', $result); + + $this->assertEquals(sizeof($result['foo@foo.com']->phones), 3); + $this->assertEquals(sizeof($result['bar@bar.com']->phones), 3); + $this->assertEquals(sizeof($result['foobar@foobar.com']->phones), 3); + + $this->assertArrayHasKey(1, $result['foo@foo.com']->phones->toArray()); + $this->assertArrayHasKey(2, $result['foo@foo.com']->phones->toArray()); + $this->assertArrayHasKey(3, $result['foo@foo.com']->phones->toArray()); + + $this->assertArrayHasKey(4, $result['bar@bar.com']->phones->toArray()); + $this->assertArrayHasKey(5, $result['bar@bar.com']->phones->toArray()); + $this->assertArrayHasKey(6, $result['bar@bar.com']->phones->toArray()); + + $this->assertArrayHasKey(7, $result['foobar@foobar.com']->phones->toArray()); + $this->assertArrayHasKey(8, $result['foobar@foobar.com']->phones->toArray()); + $this->assertArrayHasKey(9, $result['foobar@foobar.com']->phones->toArray()); + + $this->assertEquals('SELECT u, p FROM '.__NAMESPACE__ . '\DDC1135User u INDEX BY u.email INNER JOIN u.phones p INDEX BY p.id', $dql); + } + + private function loadFixture() + { + $p1 = array('11 xxxx-xxxx','11 yyyy-yyyy','11 zzzz-zzzz'); + $p2 = array('22 xxxx-xxxx','22 yyyy-yyyy','22 zzzz-zzzz'); + $p3 = array('33 xxxx-xxxx','33 yyyy-yyyy','33 zzzz-zzzz'); + + $u1 = new DDC1135User("foo@foo.com", "Foo",$p1); + $u2 = new DDC1135User("bar@bar.com", "Bar",$p2); + $u3 = new DDC1135User("foobar@foobar.com", "Foo Bar",$p3); + + $this->_em->persist($u1); + $this->_em->persist($u2); + $this->_em->persist($u3); + $this->_em->flush(); + $this->_em->clear(); + } + +} + +/** + * @Entity + */ +class DDC1135User +{ + /** + * @Id @Column(type="integer") + * @GeneratedValue + */ + public $id; + + /** + * @Column(type="string", unique=true) + */ + public $email; + + /** + * @Column(type="string") + */ + public $name; + + /** + * @OneToMany(targetEntity="DDC1135Phone", mappedBy="user", cascade={"persist", "remove"}) + */ + public $phones; + + public function __construct($email, $name, array $numbers = array()) + { + $this->name = $name; + $this->email = $email; + $this->phones = new \Doctrine\Common\Collections\ArrayCollection(); + + foreach ($numbers as $number) { + $this->phones->add(new DDC1135Phone($this,$number)); + } + } +} + +/** + * @Entity + */ +class DDC1135Phone +{ + /** + * @Id + * @Column(name="id", type="integer") + * @GeneratedValue(strategy="AUTO") + */ + public $id; + + /** + * @Column(name="number", type="string", nullable = false) + */ + public $number; + + /** + * @ManyToOne(targetEntity="DDC1135User", inversedBy="phones") + * @JoinColumn(name="user_id", referencedColumnName="id", nullable = false) + */ + public $user; + + public function __construct($user, $number) + { + $this->user = $user; + $this->number = $number; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1383Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1383Test.php new file mode 100644 index 000000000..6a8cf483a --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1383Test.php @@ -0,0 +1,99 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1383AbstractEntity'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1383Entity'), + )); + } catch(\Exception $ignored) {} + } + + public function testFailingCase() + { + $parent = new DDC1383Entity(); + $child = new DDC1383Entity(); + + $child->setReference($parent); + + $this->_em->persist($parent); + $this->_em->persist($child); + + $id = $child->getId(); + + $this->_em->flush(); + $this->_em->clear(); + + // Try merging the parent entity + $child = $this->_em->merge($child); + $parent = $child->getReference(); + + // Parent is not instance of the abstract class + self::assertTrue($parent instanceof DDC1383AbstractEntity, + "Entity class is " . get_class($parent) . ', "DDC1383AbstractEntity" was expected'); + + // Parent is NOT instance of entity + self::assertTrue($parent instanceof DDC1383Entity, + "Entity class is " . get_class($parent) . ', "DDC1383Entity" was expected'); + } +} + +/** + * @Entity + * @InheritanceType("JOINED") + * @DiscriminatorColumn(name="discr", type="integer") + * @DiscriminatorMap({1 = "DDC1383Entity"}) + */ +abstract class DDC1383AbstractEntity +{ + /** + * @Id + * @Column(type="integer") + * @GeneratedValue + */ + protected $id; + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } +} + +/** + * @Entity + */ +class DDC1383Entity extends DDC1383AbstractEntity +{ + /** + * @ManyToOne(targetEntity="DDC1383AbstractEntity") + */ + protected $reference; + + public function getReference() + { + return $this->reference; + } + + public function setReference(DDC1383AbstractEntity $reference) + { + $this->reference = $reference; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1392Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1392Test.php new file mode 100644 index 000000000..095b1976c --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1392Test.php @@ -0,0 +1,127 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1392File'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1392Picture'), + )); + } catch (\Exception $ignored) { + } + } + + public function testFailingCase() + { + $file = new DDC1392File; + + $picture = new DDC1392Picture; + $picture->setFile($file); + + $em = $this->_em; + $em->persist($picture); + $em->flush(); + $em->clear(); + + $fileId = $file->getFileId(); + $pictureId = $picture->getPictureId(); + + $this->assertTrue($fileId > 0); + + $picture = $em->find(__NAMESPACE__ . '\DDC1392Picture', $pictureId); + $this->assertEquals(UnitOfWork::STATE_MANAGED, $em->getUnitOfWork()->getEntityState($picture->getFile()), "Lazy Proxy should be marked MANAGED."); + + $file = $picture->getFile(); + + // With this activated there will be no problem + //$file->__load(); + + $picture->setFile(null); + + $em->clear(); + + $em->merge($file); + + $em->flush(); + + $q = $this->_em->createQuery("SELECT COUNT(e) FROM " . __NAMESPACE__ . '\DDC1392File e'); + $result = $q->getSingleScalarResult(); + + self::assertEquals(1, $result); + } +} + +/** + * @Entity + */ +class DDC1392Picture +{ + /** + * @Column(name="picture_id", type="integer") + * @Id @GeneratedValue + */ + private $pictureId; + + /** + * @ManyToOne(targetEntity="DDC1392File", cascade={"persist", "remove"}) + * @JoinColumn(name="file_id", referencedColumnName="file_id") + */ + private $file; + + /** + * Get pictureId + */ + public function getPictureId() + { + return $this->pictureId; + } + + /** + * Set file + */ + public function setFile($value = null) + { + $this->file = $value; + } + + /** + * Get file + */ + public function getFile() + { + return $this->file; + } +} + +/** + * @Entity + */ +class DDC1392File +{ + /** + * @Column(name="file_id", type="integer") + * @Id + * @GeneratedValue(strategy="AUTO") + */ + public $fileId; + + /** + * Get fileId + */ + public function getFileId() + { + return $this->fileId; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC381Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC381Test.php index c25a8aa75..678135daf 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC381Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC381Test.php @@ -31,8 +31,8 @@ class DDC381Test extends \Doctrine\Tests\OrmFunctionalTestCase $entity = $this->_em->getReference('Doctrine\Tests\ORM\Functional\Ticket\DDC381Entity', $persistedId); - // explicitly load proxy - $id = $entity->getId(); + // explicitly load proxy (getId() does not trigger reload of proxy) + $id = $entity->getOtherMethod(); $data = serialize($entity); $entity = unserialize($data); @@ -55,4 +55,9 @@ class DDC381Entity { return $this->id; } + + public function getOtherMethod() + { + + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/TypeTest.php b/tests/Doctrine/Tests/ORM/Functional/TypeTest.php index 25523bb80..8a2eab70e 100644 --- a/tests/Doctrine/Tests/ORM/Functional/TypeTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/TypeTest.php @@ -37,6 +37,10 @@ class TypeTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(0.1515, $decimal->highScale); } + /** + * @group DDC-1394 + * @return void + */ public function testBoolean() { $bool = new BooleanModel(); @@ -46,7 +50,7 @@ class TypeTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->flush(); $this->_em->clear(); - $dql = "SELECT b FROM Doctrine\Tests\Models\Generic\BooleanModel b"; + $dql = "SELECT b FROM Doctrine\Tests\Models\Generic\BooleanModel b WHERE b.booleanField = true"; $bool = $this->_em->createQuery($dql)->getSingleResult(); $this->assertTrue($bool->booleanField); @@ -56,7 +60,7 @@ class TypeTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->flush(); $this->_em->clear(); - $dql = "SELECT b FROM Doctrine\Tests\Models\Generic\BooleanModel b"; + $dql = "SELECT b FROM Doctrine\Tests\Models\Generic\BooleanModel b WHERE b.booleanField = false"; $bool = $this->_em->createQuery($dql)->getSingleResult(); $this->assertFalse($bool->booleanField); diff --git a/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php index 4fb5b92ac..a318f78af 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php @@ -763,4 +763,58 @@ class ArrayHydratorTest extends HydrationTestCase $this->assertEquals(1, count($result)); } + + /** + * @group DDC-1358 + */ + public function testMissingIdForRootEntity() + { + $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addFieldResult('u', 'u__id', 'id'); + $rsm->addFieldResult('u', 'u__status', 'status'); + $rsm->addScalarResult('sclr0', 'nameUpper'); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'sclr0' => 'ROMANB', + ), + array( + 'u__id' => null, + 'u__status' => null, + 'sclr0' => 'ROMANB', + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'sclr0' => 'JWAGE', + ), + array( + 'u__id' => null, + 'u__status' => null, + 'sclr0' => 'JWAGE', + ), + ); + + $stmt = new HydratorMockStatement($resultSet); + $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $rsm); + + $this->assertEquals(4, count($result), "Should hydrate four results."); + + $this->assertEquals('ROMANB', $result[0]['nameUpper']); + $this->assertEquals('ROMANB', $result[1]['nameUpper']); + $this->assertEquals('JWAGE', $result[2]['nameUpper']); + $this->assertEquals('JWAGE', $result[3]['nameUpper']); + + $this->assertEquals(array('id' => 1, 'status' => 'developer'), $result[0][0]); + $this->assertNull($result[1][0]); + $this->assertEquals(array('id' => 2, 'status' => 'developer'), $result[2][0]); + $this->assertNull($result[3][0]); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php index f2673ac70..581a3504a 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php @@ -1008,4 +1008,165 @@ class ObjectHydratorTest extends HydrationTestCase $this->assertEquals(4, count($result[1]->groups)); $this->assertEquals(2, count($result[1]->phonenumbers)); } + + /** + * @group DDC-1358 + */ + public function testMissingIdForRootEntity() + { + $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addFieldResult('u', 'u__id', 'id'); + $rsm->addFieldResult('u', 'u__status', 'status'); + $rsm->addScalarResult('sclr0', 'nameUpper'); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'sclr0' => 'ROMANB', + ), + array( + 'u__id' => null, + 'u__status' => null, + 'sclr0' => 'ROMANB', + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'sclr0' => 'JWAGE', + ), + array( + 'u__id' => null, + 'u__status' => null, + 'sclr0' => 'JWAGE', + ), + ); + + $stmt = new HydratorMockStatement($resultSet); + $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + + $this->assertEquals(4, count($result), "Should hydrate four results."); + + $this->assertEquals('ROMANB', $result[0]['nameUpper']); + $this->assertEquals('ROMANB', $result[1]['nameUpper']); + $this->assertEquals('JWAGE', $result[2]['nameUpper']); + $this->assertEquals('JWAGE', $result[3]['nameUpper']); + + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); + $this->assertNull($result[1][0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[2][0]); + $this->assertNull($result[3][0]); + } + + /** + * @group DDC-1358 + * @return void + */ + public function testMissingIdForCollectionValuedChildEntity() + { + $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addJoinedEntityResult( + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' + ); + $rsm->addFieldResult('u', 'u__id', 'id'); + $rsm->addFieldResult('u', 'u__status', 'status'); + $rsm->addScalarResult('sclr0', 'nameUpper'); + $rsm->addFieldResult('p', 'p__phonenumber', 'phonenumber'); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'sclr0' => 'ROMANB', + 'p__phonenumber' => '42', + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'sclr0' => 'ROMANB', + 'p__phonenumber' => null + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'sclr0' => 'JWAGE', + 'p__phonenumber' => '91' + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'sclr0' => 'JWAGE', + 'p__phonenumber' => null + ) + ); + + $stmt = new HydratorMockStatement($resultSet); + $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + + $this->assertEquals(2, count($result)); + $this->assertEquals(1, $result[0][0]->phonenumbers->count()); + $this->assertEquals(1, $result[1][0]->phonenumbers->count()); + } + + /** + * @group DDC-1358 + */ + public function testMissingIdForSingleValuedChildEntity() + { + $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addJoinedEntityResult( + 'Doctrine\Tests\Models\CMS\CmsAddress', + 'a', + 'u', + 'address' + ); + $rsm->addFieldResult('u', 'u__id', 'id'); + $rsm->addFieldResult('u', 'u__status', 'status'); + $rsm->addScalarResult('sclr0', 'nameUpper'); + $rsm->addFieldResult('a', 'a__id', 'id'); + $rsm->addFieldResult('a', 'a__city', 'city'); + $rsm->addMetaResult('a', 'user_id', 'user_id'); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'sclr0' => 'ROMANB', + 'a__id' => 1, + 'a__city' => 'Berlin', + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'sclr0' => 'BENJAMIN', + 'a__id' => null, + 'a__city' => null, + ), + ); + + $stmt = new HydratorMockStatement($resultSet); + $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + + $this->assertEquals(2, count($result)); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsAddress', $result[0][0]->address); + $this->assertNull($result[1][0]->address); + } } diff --git a/tests/Doctrine/Tests/ORM/Mapping/BasicInheritanceMappingTest.php b/tests/Doctrine/Tests/ORM/Mapping/BasicInheritanceMappingTest.php index 93504ec53..1d8d6c118 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/BasicInheritanceMappingTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/BasicInheritanceMappingTest.php @@ -114,7 +114,7 @@ class BasicInheritanceMappingTest extends \Doctrine\Tests\OrmTestCase */ public function testUnmappedEntityInHierachy() { - $this->setExpectedException('Doctrine\ORM\Mapping\MappingException', "Entity 'Doctrine\Tests\ORM\Mapping\HierachyBEntity' has to be part of the descriminator map of 'Doctrine\Tests\ORM\Mapping\HierachyBase' to be properly mapped in the inheritance hierachy. Alternatively you can make 'Doctrine\Tests\ORM\Mapping\HierachyBEntity' an abstract class to avoid this exception from occuring."); + $this->setExpectedException('Doctrine\ORM\Mapping\MappingException', "Entity 'Doctrine\Tests\ORM\Mapping\HierachyBEntity' has to be part of the discriminator map of 'Doctrine\Tests\ORM\Mapping\HierachyBase' to be properly mapped in the inheritance hierachy. Alternatively you can make 'Doctrine\Tests\ORM\Mapping\HierachyBEntity' an abstract class to avoid this exception from occuring."); $class = $this->_factory->getMetadataFor(__NAMESPACE__ . '\\HierachyE'); } diff --git a/tests/Doctrine/Tests/ORM/Mapping/Symfony/AbstractDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/Symfony/AbstractDriverTest.php new file mode 100644 index 000000000..d16db4fbb --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/Symfony/AbstractDriverTest.php @@ -0,0 +1,113 @@ +. +*/ + +namespace Doctrine\Tests\ORM\Mapping\Symfony; + +/** + * @group DDC-1418 + */ +abstract class AbstractDriverTest extends \PHPUnit_Framework_TestCase +{ + public function testFindMappingFile() + { + $driver = $this->getDriver(array( + 'MyNamespace\MySubnamespace\EntityFoo' => 'foo', + 'MyNamespace\MySubnamespace\Entity' => $this->dir, + )); + + touch($filename = $this->dir.'/Foo'.$this->getFileExtension()); + $this->assertEquals($filename, $this->invoke($driver, '_findMappingFile', array('MyNamespace\MySubnamespace\Entity\Foo'))); + } + + public function testFindMappingFileInSubnamespace() + { + $driver = $this->getDriver(array( + 'MyNamespace\MySubnamespace\Entity' => $this->dir, + )); + + touch($filename = $this->dir.'/Foo.Bar'.$this->getFileExtension()); + $this->assertEquals($filename, $this->invoke($driver, '_findMappingFile', array('MyNamespace\MySubnamespace\Entity\Foo\Bar'))); + } + + public function testFindMappingFileNamespacedFoundFileNotFound() + { + $this->setExpectedException( + 'Doctrine\ORM\Mapping\MappingException', + "No mapping file found named '".$this->dir."/Foo".$this->getFileExtension()."' for class 'MyNamespace\MySubnamespace\Entity\Foo'." + ); + + $driver = $this->getDriver(array( + 'MyNamespace\MySubnamespace\Entity' => $this->dir, + )); + + $this->invoke($driver, '_findMappingFile', array('MyNamespace\MySubnamespace\Entity\Foo')); + } + + public function testFindMappingNamespaceNotFound() + { + $this->setExpectedException( + 'Doctrine\ORM\Mapping\MappingException', + "No mapping file found named 'Foo".$this->getFileExtension()."' for class 'MyOtherNamespace\MySubnamespace\Entity\Foo'." + ); + + $driver = $this->getDriver(array( + 'MyNamespace\MySubnamespace\Entity' => $this->dir, + )); + + $this->invoke($driver, '_findMappingFile', array('MyOtherNamespace\MySubnamespace\Entity\Foo')); + } + + protected function setUp() + { + $this->dir = sys_get_temp_dir().'/abstract_driver_test'; + @mkdir($this->dir, 0777, true); + } + + protected function tearDown() + { + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->dir), \RecursiveIteratorIterator::CHILD_FIRST); + + foreach ($iterator as $path) { + if ($path->isDir()) { + @rmdir($path); + } else { + @unlink($path); + } + } + + @rmdir($this->dir); + } + + abstract protected function getFileExtension(); + abstract protected function getDriver(array $paths = array()); + + private function setField($obj, $field, $value) + { + $ref = new \ReflectionProperty($obj, $field); + $ref->setAccessible(true); + $ref->setValue($obj, $value); + } + + private function invoke($obj, $method, array $args = array()) { + $ref = new \ReflectionMethod($obj, $method); + $ref->setAccessible(true); + + return $ref->invokeArgs($obj, $args); + } +} diff --git a/tests/Doctrine/Tests/ORM/Mapping/Symfony/XmlDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/Symfony/XmlDriverTest.php new file mode 100644 index 000000000..5908b674a --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/Symfony/XmlDriverTest.php @@ -0,0 +1,40 @@ +. +*/ + +namespace Doctrine\Tests\ORM\Mapping\Symfony; + +use \Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver; + +/** + * @group DDC-1418 + */ +class XmlDriverTest extends AbstractDriverTest +{ + protected function getFileExtension() + { + return '.orm.xml'; + } + + protected function getDriver(array $paths = array()) + { + $driver = new SimplifiedXmlDriver(array_flip($paths)); + + return $driver; + } +} diff --git a/tests/Doctrine/Tests/ORM/Mapping/Symfony/YamlDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/Symfony/YamlDriverTest.php new file mode 100644 index 000000000..c5d8d1cd1 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/Symfony/YamlDriverTest.php @@ -0,0 +1,40 @@ +. +*/ + +namespace Doctrine\Tests\ORM\Mapping\Symfony; + +use \Doctrine\ORM\Mapping\Driver\SimplifiedYamlDriver; + +/** + * @group DDC-1418 + */ +class YamlDriverTest extends AbstractDriverTest +{ + protected function getFileExtension() + { + return '.orm.yml'; + } + + protected function getDriver(array $paths = array()) + { + $driver = new SimplifiedYamlDriver(array_flip($paths)); + + return $driver; + } +} diff --git a/tests/Doctrine/Tests/ORM/Proxy/ProxyClassGeneratorTest.php b/tests/Doctrine/Tests/ORM/Proxy/ProxyClassGeneratorTest.php index 6257dbe70..39bdcfe5a 100644 --- a/tests/Doctrine/Tests/ORM/Proxy/ProxyClassGeneratorTest.php +++ b/tests/Doctrine/Tests/ORM/Proxy/ProxyClassGeneratorTest.php @@ -73,12 +73,13 @@ class ProxyClassGeneratorTest extends \Doctrine\Tests\OrmTestCase $persister = $this->_getMockPersister(); $this->_uowMock->setEntityPersister('Doctrine\Tests\Models\ECommerce\ECommerceFeature', $persister); $proxy = $this->_proxyFactory->getProxy('Doctrine\Tests\Models\ECommerce\ECommerceFeature', $identifier); + $persister->expects($this->atLeastOnce()) ->method('load') ->with($this->equalTo($identifier), $this->isInstanceOf($proxyClass)) ->will($this->returnValue(new \stdClass())); // fake return of entity instance - $proxy->getId(); $proxy->getDescription(); + $proxy->getProduct(); } public function testReferenceProxyRespectsMethodsParametersTypeHinting() @@ -179,4 +180,4 @@ class SleepClass { return array('id'); } -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 6ef886035..53002a8fc 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -33,7 +33,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase } $query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true) - ->useQueryCache(false); + ->useQueryCache(false); foreach ($queryHints AS $name => $value) { $query->setHint($name, $value); @@ -65,7 +65,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase } $query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true) - ->useQueryCache(false); + ->useQueryCache(false); foreach ($queryHints AS $name => $value) { $query->setHint($name, $value); @@ -729,12 +729,12 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase $this->assertSqlGeneration( "SELECT b FROM Doctrine\Tests\Models\Generic\BooleanModel b WHERE b.booleanField = true", - "SELECT b0_.id AS id0, b0_.booleanField AS booleanField1 FROM boolean_model b0_ WHERE b0_.booleanField = 'true'" + "SELECT b0_.id AS id0, b0_.booleanField AS booleanField1 FROM boolean_model b0_ WHERE b0_.booleanField = true" ); $this->assertSqlGeneration( "SELECT b FROM Doctrine\Tests\Models\Generic\BooleanModel b WHERE b.booleanField = false", - "SELECT b0_.id AS id0, b0_.booleanField AS booleanField1 FROM boolean_model b0_ WHERE b0_.booleanField = 'false'" + "SELECT b0_.id AS id0, b0_.booleanField AS booleanField1 FROM boolean_model b0_ WHERE b0_.booleanField = false" ); $this->_em->getConnection()->setDatabasePlatform($oldPlat); @@ -1095,6 +1095,162 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase "Doctrine\ORM\Query\QueryException" ); } + + /** + * @group DDC-1389 + */ + public function testInheritanceTypeJoinInRootClassWithDisabledForcePartialLoad() + { + $this->assertSqlGeneration( + 'SELECT p FROM Doctrine\Tests\Models\Company\CompanyPerson p', + 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.title AS title2, c1_.car_id AS car_id3, c2_.salary AS salary4, c2_.department AS department5, c2_.startDate AS startDate6, c0_.discr AS discr7, c0_.spouse_id AS spouse_id8 FROM company_persons c0_ LEFT JOIN company_managers c1_ ON c0_.id = c1_.id LEFT JOIN company_employees c2_ ON c0_.id = c2_.id', + array(Query::HINT_FORCE_PARTIAL_LOAD => false) + ); + } + + /** + * @group DDC-1389 + */ + public function testInheritanceTypeJoinInRootClassWithEnabledForcePartialLoad() + { + $this->assertSqlGeneration( + 'SELECT p FROM Doctrine\Tests\Models\Company\CompanyPerson p', + 'SELECT c0_.id AS id0, c0_.name AS name1, c0_.discr AS discr2 FROM company_persons c0_', + array(Query::HINT_FORCE_PARTIAL_LOAD => true) + ); + } + + /** + * @group DDC-1389 + */ + public function testInheritanceTypeJoinInChildClassWithDisabledForcePartialLoad() + { + $this->assertSqlGeneration( + 'SELECT e FROM Doctrine\Tests\Models\Company\CompanyEmployee e', + 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.salary AS salary2, c1_.department AS department3, c1_.startDate AS startDate4, c2_.title AS title5, c2_.car_id AS car_id6, c0_.discr AS discr7, c0_.spouse_id AS spouse_id8 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id LEFT JOIN company_managers c2_ ON c1_.id = c2_.id', + array(Query::HINT_FORCE_PARTIAL_LOAD => false) + ); + } + + /** + * @group DDC-1389 + */ + public function testInheritanceTypeJoinInChildClassWithEnabledForcePartialLoad() + { + $this->assertSqlGeneration( + 'SELECT e FROM Doctrine\Tests\Models\Company\CompanyEmployee e', + 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.salary AS salary2, c1_.department AS department3, c1_.startDate AS startDate4, c0_.discr AS discr5 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id', + array(Query::HINT_FORCE_PARTIAL_LOAD => true) + ); + } + + /** + * @group DDC-1389 + */ + public function testInheritanceTypeJoinInLeafClassWithDisabledForcePartialLoad() + { + $this->assertSqlGeneration( + 'SELECT m FROM Doctrine\Tests\Models\Company\CompanyManager m', + 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.salary AS salary2, c1_.department AS department3, c1_.startDate AS startDate4, c2_.title AS title5, c0_.discr AS discr6, c0_.spouse_id AS spouse_id7, c2_.car_id AS car_id8 FROM company_managers c2_ INNER JOIN company_employees c1_ ON c2_.id = c1_.id INNER JOIN company_persons c0_ ON c2_.id = c0_.id', + array(Query::HINT_FORCE_PARTIAL_LOAD => false) + ); + } + + /** + * @group DDC-1389 + */ + public function testInheritanceTypeJoinInLeafClassWithEnabledForcePartialLoad() + { + $this->assertSqlGeneration( + 'SELECT m FROM Doctrine\Tests\Models\Company\CompanyManager m', + 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.salary AS salary2, c1_.department AS department3, c1_.startDate AS startDate4, c2_.title AS title5, c0_.discr AS discr6 FROM company_managers c2_ INNER JOIN company_employees c1_ ON c2_.id = c1_.id INNER JOIN company_persons c0_ ON c2_.id = c0_.id', + array(Query::HINT_FORCE_PARTIAL_LOAD => true) + ); + } + + /** + * @group DDC-1389 + */ + public function testInheritanceTypeSingleTableInRootClassWithDisabledForcePartialLoad() + { + $this->assertSqlGeneration( + 'SELECT c FROM Doctrine\Tests\Models\Company\CompanyContract c', + "SELECT c0_.id AS id0, c0_.completed AS completed1, c0_.fixPrice AS fixPrice2, c0_.hoursWorked AS hoursWorked3, c0_.pricePerHour AS pricePerHour4, c0_.maxPrice AS maxPrice5, c0_.discr AS discr6, c0_.salesPerson_id AS salesPerson_id7 FROM company_contracts c0_ WHERE c0_.discr IN ('fix', 'flexible', 'flexultra')", + array(Query::HINT_FORCE_PARTIAL_LOAD => false) + ); + } + + /** + * @group DDC-1389 + */ + public function testInheritanceTypeSingleTableInRootClassWithEnabledForcePartialLoad() + { + $this->assertSqlGeneration( + 'SELECT c FROM Doctrine\Tests\Models\Company\CompanyContract c', + "SELECT c0_.id AS id0, c0_.completed AS completed1, c0_.fixPrice AS fixPrice2, c0_.hoursWorked AS hoursWorked3, c0_.pricePerHour AS pricePerHour4, c0_.maxPrice AS maxPrice5, c0_.discr AS discr6 FROM company_contracts c0_ WHERE c0_.discr IN ('fix', 'flexible', 'flexultra')", + array(Query::HINT_FORCE_PARTIAL_LOAD => true) + ); + } + + /** + * @group DDC-1389 + */ + public function testInheritanceTypeSingleTableInChildClassWithDisabledForcePartialLoad() + { + $this->assertSqlGeneration( + 'SELECT fc FROM Doctrine\Tests\Models\Company\CompanyFlexContract fc', + "SELECT c0_.id AS id0, c0_.completed AS completed1, c0_.hoursWorked AS hoursWorked2, c0_.pricePerHour AS pricePerHour3, c0_.maxPrice AS maxPrice4, c0_.discr AS discr5, c0_.salesPerson_id AS salesPerson_id6 FROM company_contracts c0_ WHERE c0_.discr IN ('flexible', 'flexultra')", + array(Query::HINT_FORCE_PARTIAL_LOAD => false) + ); + } + + /** + * @group DDC-1389 + */ + public function testInheritanceTypeSingleTableInChildClassWithEnabledForcePartialLoad() + { + $this->assertSqlGeneration( + 'SELECT fc FROM Doctrine\Tests\Models\Company\CompanyFlexContract fc', + "SELECT c0_.id AS id0, c0_.completed AS completed1, c0_.hoursWorked AS hoursWorked2, c0_.pricePerHour AS pricePerHour3, c0_.maxPrice AS maxPrice4, c0_.discr AS discr5 FROM company_contracts c0_ WHERE c0_.discr IN ('flexible', 'flexultra')", + array(Query::HINT_FORCE_PARTIAL_LOAD => true) + ); + } + + /** + * @group DDC-1389 + */ + public function testInheritanceTypeSingleTableInLeafClassWithDisabledForcePartialLoad() + { + $this->assertSqlGeneration( + 'SELECT fuc FROM Doctrine\Tests\Models\Company\CompanyFlexUltraContract fuc', + "SELECT c0_.id AS id0, c0_.completed AS completed1, c0_.hoursWorked AS hoursWorked2, c0_.pricePerHour AS pricePerHour3, c0_.maxPrice AS maxPrice4, c0_.discr AS discr5, c0_.salesPerson_id AS salesPerson_id6 FROM company_contracts c0_ WHERE c0_.discr IN ('flexultra')", + array(Query::HINT_FORCE_PARTIAL_LOAD => false) + ); + } + + /** + * @group DDC-1389 + */ + public function testInheritanceTypeSingleTableInLeafClassWithEnabledForcePartialLoad() + { + $this->assertSqlGeneration( + 'SELECT fuc FROM Doctrine\Tests\Models\Company\CompanyFlexUltraContract fuc', + "SELECT c0_.id AS id0, c0_.completed AS completed1, c0_.hoursWorked AS hoursWorked2, c0_.pricePerHour AS pricePerHour3, c0_.maxPrice AS maxPrice4, c0_.discr AS discr5 FROM company_contracts c0_ WHERE c0_.discr IN ('flexultra')", + array(Query::HINT_FORCE_PARTIAL_LOAD => true) + ); + } + + /** + * @group DDC-1161 + */ + public function testSelfReferenceWithOneToOneDoesNotDuplicateAlias() + { + $this->assertSqlGeneration( + 'SELECT p, pp FROM Doctrine\Tests\Models\Company\CompanyPerson p JOIN p.spouse pp', + "SELECT c0_.id AS id0, c0_.name AS name1, c1_.title AS title2, c1_.car_id AS car_id3, c2_.salary AS salary4, c2_.department AS department5, c2_.startDate AS startDate6, c3_.id AS id7, c3_.name AS name8, c4_.title AS title9, c4_.car_id AS car_id10, c5_.salary AS salary11, c5_.department AS department12, c5_.startDate AS startDate13, c0_.discr AS discr14, c0_.spouse_id AS spouse_id15, c3_.discr AS discr16, c3_.spouse_id AS spouse_id17 FROM company_persons c0_ LEFT JOIN company_managers c1_ ON c0_.id = c1_.id LEFT JOIN company_employees c2_ ON c0_.id = c2_.id INNER JOIN company_persons c3_ ON c0_.spouse_id = c3_.id LEFT JOIN company_managers c4_ ON c3_.id = c4_.id LEFT JOIN company_employees c5_ ON c3_.id = c5_.id", + array(Query::HINT_FORCE_PARTIAL_LOAD => false) + ); + } } diff --git a/tests/Doctrine/Tests/ORM/Tools/EntityGeneratorTest.php b/tests/Doctrine/Tests/ORM/Tools/EntityGeneratorTest.php index fce7d4c20..f2b298159 100644 --- a/tests/Doctrine/Tests/ORM/Tools/EntityGeneratorTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/EntityGeneratorTest.php @@ -21,6 +21,7 @@ class EntityGeneratorTest extends \Doctrine\Tests\OrmTestCase $this->_tmpDir = \sys_get_temp_dir(); \mkdir($this->_tmpDir . \DIRECTORY_SEPARATOR . $this->_namespace); $this->_generator = new EntityGenerator(); + $this->_generator->setAnnotationPrefix(""); $this->_generator->setGenerateAnnotations(true); $this->_generator->setGenerateStubMethods(true); $this->_generator->setRegenerateEntityIfExists(false); @@ -179,14 +180,15 @@ class EntityGeneratorTest extends \Doctrine\Tests\OrmTestCase public function testLoadPrefixedMetadata() { - $this->_generator->setAnnotationPrefix('orm:'); + $this->_generator->setAnnotationPrefix('ORM\\'); $metadata = $this->generateBookEntityFixture(); + $reader = new \Doctrine\Common\Annotations\AnnotationReader(); + $driver = new \Doctrine\ORM\Mapping\Driver\AnnotationDriver($reader, array()); $book = $this->newInstance($metadata); $cm = new \Doctrine\ORM\Mapping\ClassMetadata($metadata->name); - $driver = $this->createAnnotationDriver(array(), 'orm'); $driver->loadMetadataForClass($cm->name, $cm); $this->assertEquals($cm->columnNames, $metadata->columnNames); diff --git a/tests/Doctrine/Tests/ORM/Tools/Export/AbstractClassMetadataExporterTest.php b/tests/Doctrine/Tests/ORM/Tools/Export/AbstractClassMetadataExporterTest.php index c9bfdccf0..2571a1b98 100644 --- a/tests/Doctrine/Tests/ORM/Tools/Export/AbstractClassMetadataExporterTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/Export/AbstractClassMetadataExporterTest.php @@ -98,6 +98,8 @@ abstract class AbstractClassMetadataExporterTest extends \Doctrine\Tests\OrmTest public function testExportDirectoryAndFilesAreCreated() { + $this->_deleteDirectory(__DIR__ . '/export/'.$this->_getType()); + $type = $this->_getType(); $metadataDriver = $this->_createMetadataDriver($type, __DIR__ . '/' . $type); $em = $this->_createEntityManager($metadataDriver); @@ -113,6 +115,7 @@ abstract class AbstractClassMetadataExporterTest extends \Doctrine\Tests\OrmTest $exporter = $cme->getExporter($type, __DIR__ . '/export/' . $type); if ($type === 'annotation') { $entityGenerator = new EntityGenerator(); + $entityGenerator->setAnnotationPrefix(""); $exporter->setEntityGenerator($entityGenerator); } $this->_extension = $exporter->getExtension(); @@ -139,6 +142,8 @@ abstract class AbstractClassMetadataExporterTest extends \Doctrine\Tests\OrmTest $cmf = $this->_createClassMetadataFactory($em, $type); $metadata = $cmf->getAllMetadata(); + $this->assertEquals(1, count($metadata)); + $class = current($metadata); $this->assertEquals('Doctrine\Tests\ORM\Tools\Export\ExportedUser', $class->name); @@ -322,8 +327,7 @@ abstract class AbstractClassMetadataExporterTest extends \Doctrine\Tests\OrmTest public function __destruct() { - $type = $this->_getType(); - $this->_deleteDirectory(__DIR__ . '/export/'.$this->_getType()); +# $this->_deleteDirectory(__DIR__ . '/export/'.$this->_getType()); } protected function _deleteDirectory($path) diff --git a/tests/Doctrine/Tests/OrmTestCase.php b/tests/Doctrine/Tests/OrmTestCase.php index 678478633..fa9938d09 100644 --- a/tests/Doctrine/Tests/OrmTestCase.php +++ b/tests/Doctrine/Tests/OrmTestCase.php @@ -11,6 +11,7 @@ abstract class OrmTestCase extends DoctrineTestCase { /** The metadata cache that is shared between all ORM tests (except functional tests). */ private static $_metadataCacheImpl = null; + /** The query cache that is shared between all ORM tests (except functional tests). */ private static $_queryCacheImpl = null; @@ -66,30 +67,31 @@ abstract class OrmTestCase extends DoctrineTestCase */ protected function _getTestEntityManager($conn = null, $conf = null, $eventManager = null, $withSharedMetadata = true) { + $metadataCache = $withSharedMetadata + ? self::getSharedMetadataCacheImpl() + : new \Doctrine\Common\Cache\ArrayCache; + $config = new \Doctrine\ORM\Configuration(); - if($withSharedMetadata) { - $config->setMetadataCacheImpl(self::getSharedMetadataCacheImpl()); - } else { - $config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache); - } - + + $config->setMetadataCacheImpl($metadataCache); $config->setMetadataDriverImpl($config->newDefaultAnnotationDriver()); - $config->setQueryCacheImpl(self::getSharedQueryCacheImpl()); $config->setProxyDir(__DIR__ . '/Proxies'); $config->setProxyNamespace('Doctrine\Tests\Proxies'); - $eventManager = new \Doctrine\Common\EventManager(); + if ($conn === null) { $conn = array( - 'driverClass' => 'Doctrine\Tests\Mocks\DriverMock', + 'driverClass' => 'Doctrine\Tests\Mocks\DriverMock', 'wrapperClass' => 'Doctrine\Tests\Mocks\ConnectionMock', - 'user' => 'john', - 'password' => 'wayne' + 'user' => 'john', + 'password' => 'wayne' ); } + if (is_array($conn)) { $conn = \Doctrine\DBAL\DriverManager::getConnection($conn, $config, $eventManager); } + return \Doctrine\Tests\Mocks\EntityManagerMock::create($conn, $config, $eventManager); } @@ -98,6 +100,7 @@ abstract class OrmTestCase extends DoctrineTestCase if (self::$_metadataCacheImpl === null) { self::$_metadataCacheImpl = new \Doctrine\Common\Cache\ArrayCache; } + return self::$_metadataCacheImpl; } @@ -106,6 +109,7 @@ abstract class OrmTestCase extends DoctrineTestCase if (self::$_queryCacheImpl === null) { self::$_queryCacheImpl = new \Doctrine\Common\Cache\ArrayCache; } + return self::$_queryCacheImpl; } }