From 81cc6d9da83d217ea62bd467053fd9885d1083cd Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Mon, 14 Nov 2011 01:36:39 -0200 Subject: [PATCH] Implemented alias support for EntityResult. This addresses DDC-1096 and DDC-1424. Improved DQL Parser, SQL Walker and Hydrators in general. Performance is generally improved by a factor of 20%. There is still more to be done, like remove the isMixed in ResultSetMapping, mainly because this query - SELECT u AS user FROM User u -, it should return an array('user' => [User object]), while currently it doesn't due to this before mentioned 'bug' in RSM. Will open a separate ticket for this. Also, UnitOfWork and Hydrators share code that could be abstracted/improved. --- .gitignore | 3 + .../Internal/Hydration/AbstractHydrator.php | 165 ++-- .../ORM/Internal/Hydration/ArrayHydrator.php | 74 +- .../ORM/Internal/Hydration/ObjectHydrator.php | 122 ++- lib/Doctrine/ORM/Query/Parser.php | 811 ++++++++++-------- lib/Doctrine/ORM/Query/ResultSetMapping.php | 115 +-- lib/Doctrine/ORM/Query/SqlWalker.php | 130 +-- .../Tests/ORM/Hydration/ArrayHydratorTest.php | 459 +++++----- .../ORM/Hydration/ObjectHydratorTest.php | 706 ++++++++------- 9 files changed, 1436 insertions(+), 1149 deletions(-) diff --git a/.gitignore b/.gitignore index 04f63f22d..329249d72 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ download/ lib/api/ lib/Doctrine/Common lib/Doctrine/DBAL +/.settings/ +.buildpath +.project diff --git a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php index 493b1461c..146dfb5c5 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php @@ -74,7 +74,7 @@ abstract class AbstractHydrator * * @param object $stmt * @param object $resultSetMapping - * + * * @return IterableResult */ public function iterate($stmt, $resultSetMapping, array $hints = array()) @@ -82,9 +82,9 @@ abstract class AbstractHydrator $this->_stmt = $stmt; $this->_rsm = $resultSetMapping; $this->_hints = $hints; - + $this->prepare(); - + return new IterableResult($this); } @@ -100,13 +100,13 @@ abstract class AbstractHydrator $this->_stmt = $stmt; $this->_rsm = $resultSetMapping; $this->_hints = $hints; - + $this->prepare(); - + $result = $this->hydrateAllData(); - + $this->cleanup(); - + return $result; } @@ -119,17 +119,17 @@ abstract class AbstractHydrator public function hydrateRow() { $row = $this->_stmt->fetch(PDO::FETCH_ASSOC); - + if ( ! $row) { $this->cleanup(); - + return false; } - + $result = array(); - + $this->hydrateRowData($row, $this->_cache, $result); - + return $result; } @@ -147,7 +147,7 @@ abstract class AbstractHydrator protected function cleanup() { $this->_rsm = null; - + $this->_stmt->closeCursor(); $this->_stmt = null; } @@ -195,34 +195,44 @@ abstract class AbstractHydrator foreach ($data as $key => $value) { // Parse each column name only once. Cache the results. if ( ! isset($cache[$key])) { - if (isset($this->_rsm->scalarMappings[$key])) { - $cache[$key]['fieldName'] = $this->_rsm->scalarMappings[$key]; - $cache[$key]['isScalar'] = true; - } else if (isset($this->_rsm->fieldMappings[$key])) { - $fieldName = $this->_rsm->fieldMappings[$key]; - $classMetadata = $this->_em->getClassMetadata($this->_rsm->declaringClasses[$key]); - $cache[$key]['fieldName'] = $fieldName; - $cache[$key]['type'] = Type::getType($classMetadata->fieldMappings[$fieldName]['type']); - $cache[$key]['isIdentifier'] = $classMetadata->isIdentifier($fieldName); - $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; - } else if (!isset($this->_rsm->metaMappings[$key])) { - // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2 - // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping. - continue; - } else { - // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns). - $fieldName = $this->_rsm->metaMappings[$key]; - $cache[$key]['isMetaColumn'] = true; - $cache[$key]['fieldName'] = $fieldName; - $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; - $classMetadata = $this->_em->getClassMetadata($this->_rsm->aliasMap[$cache[$key]['dqlAlias']]); - $cache[$key]['isIdentifier'] = isset($this->_rsm->isIdentifierColumn[$cache[$key]['dqlAlias']][$key]); + switch (true) { + // NOTE: Most of the times it's a field mapping, so keep it first!!! + case (isset($this->_rsm->fieldMappings[$key])): + $fieldName = $this->_rsm->fieldMappings[$key]; + $classMetadata = $this->_em->getClassMetadata($this->_rsm->declaringClasses[$key]); + + $cache[$key]['fieldName'] = $fieldName; + $cache[$key]['type'] = Type::getType($classMetadata->fieldMappings[$fieldName]['type']); + $cache[$key]['isIdentifier'] = $classMetadata->isIdentifier($fieldName); + $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; + break; + + case (isset($this->_rsm->scalarMappings[$key])): + $cache[$key]['fieldName'] = $this->_rsm->scalarMappings[$key]; + $cache[$key]['isScalar'] = true; + break; + + case (isset($this->_rsm->metaMappings[$key])): + // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns). + $fieldName = $this->_rsm->metaMappings[$key]; + $classMetadata = $this->_em->getClassMetadata($this->_rsm->aliasMap[$this->_rsm->columnOwnerMap[$key]]); + + $cache[$key]['isMetaColumn'] = true; + $cache[$key]['fieldName'] = $fieldName; + $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; + $cache[$key]['isIdentifier'] = isset($this->_rsm->isIdentifierColumn[$cache[$key]['dqlAlias']][$key]); + break; + + default: + // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2 + // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping. + continue 2; } } if (isset($cache[$key]['isScalar'])) { $rowData['scalars'][$cache[$key]['fieldName']] = $value; - + continue; } @@ -233,10 +243,10 @@ abstract class AbstractHydrator } if (isset($cache[$key]['isMetaColumn'])) { - if (!isset($rowData[$dqlAlias][$cache[$key]['fieldName']]) || $value !== null) { + if ( ! isset($rowData[$dqlAlias][$cache[$key]['fieldName']]) || $value !== null) { $rowData[$dqlAlias][$cache[$key]['fieldName']] = $value; } - + continue; } @@ -259,7 +269,7 @@ abstract class AbstractHydrator /** * Processes a row of the result set. - * + * * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that * simply converts column names to field names and properly converts the * values according to their types. The resulting row has the same number @@ -267,7 +277,7 @@ abstract class AbstractHydrator * * @param array $data * @param array $cache - * + * * @return array The processed row. */ protected function gatherScalarRowData(&$data, &$cache) @@ -277,48 +287,65 @@ abstract class AbstractHydrator foreach ($data as $key => $value) { // Parse each column name only once. Cache the results. if ( ! isset($cache[$key])) { - if (isset($this->_rsm->scalarMappings[$key])) { - $cache[$key]['fieldName'] = $this->_rsm->scalarMappings[$key]; - $cache[$key]['isScalar'] = true; - } else if (isset($this->_rsm->fieldMappings[$key])) { - $fieldName = $this->_rsm->fieldMappings[$key]; - $classMetadata = $this->_em->getClassMetadata($this->_rsm->declaringClasses[$key]); - $cache[$key]['fieldName'] = $fieldName; - $cache[$key]['type'] = Type::getType($classMetadata->fieldMappings[$fieldName]['type']); - $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; - } else if (!isset($this->_rsm->metaMappings[$key])) { - // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2 - // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping. - continue; - } else { - // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns). - $cache[$key]['isMetaColumn'] = true; - $cache[$key]['fieldName'] = $this->_rsm->metaMappings[$key]; - $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; + switch (true) { + // NOTE: During scalar hydration, most of the times it's a scalar mapping, keep it first!!! + case (isset($this->_rsm->scalarMappings[$key])): + $cache[$key]['fieldName'] = $this->_rsm->scalarMappings[$key]; + $cache[$key]['isScalar'] = true; + break; + + case (isset($this->_rsm->fieldMappings[$key])): + $fieldName = $this->_rsm->fieldMappings[$key]; + $classMetadata = $this->_em->getClassMetadata($this->_rsm->declaringClasses[$key]); + + $cache[$key]['fieldName'] = $fieldName; + $cache[$key]['type'] = Type::getType($classMetadata->fieldMappings[$fieldName]['type']); + $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; + break; + + case (isset($this->_rsm->metaMappings[$key])): + // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns). + $cache[$key]['isMetaColumn'] = true; + $cache[$key]['fieldName'] = $this->_rsm->metaMappings[$key]; + $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; + break; + + default: + // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2 + // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping. + continue 2; } } $fieldName = $cache[$key]['fieldName']; - if (isset($cache[$key]['isScalar'])) { - $rowData[$fieldName] = $value; - } else if (isset($cache[$key]['isMetaColumn'])) { - $rowData[$cache[$key]['dqlAlias'] . '_' . $fieldName] = $value; - } else { - $rowData[$cache[$key]['dqlAlias'] . '_' . $fieldName] = $cache[$key]['type'] - ->convertToPHPValue($value, $this->_platform); + switch (true) { + case (isset($cache[$key]['isScalar'])): + $rowData[$fieldName] = $value; + break; + + case (isset($cache[$key]['isMetaColumn'])): + $rowData[$cache[$key]['dqlAlias'] . '_' . $fieldName] = $value; + break; + + default: + $value = $cache[$key]['type']->convertToPHPValue($value, $this->_platform); + + $rowData[$cache[$key]['dqlAlias'] . '_' . $fieldName] = $value; } } return $rowData; } - + /** * Register entity as managed in UnitOfWork. - * + * * @param Doctrine\ORM\Mapping\ClassMetadata $class * @param object $entity - * @param array $data + * @param array $data + * + * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow */ protected function registerManaged(ClassMetadata $class, $entity, array $data) { @@ -338,7 +365,7 @@ abstract class AbstractHydrator $id = array($class->identifier[0] => $data[$class->identifier[0]]); } } - + $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data); } } diff --git a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php index e25df2fea..817e30baf 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php @@ -28,6 +28,11 @@ use PDO, Doctrine\DBAL\Connection, Doctrine\ORM\Mapping\ClassMetadata; * @since 2.0 * @author Roman Borschel * @author Guilherme Blanco + * + * @todo General behavior is "wrong" if you define an alias to selected IdentificationVariable. + * Example: SELECT u AS user FROM User u + * The result should contains an array where each array index is an array: array('user' => [User object]) + * Problem must be solved somehow by removing the isMixed in ResultSetMapping */ class ArrayHydrator extends AbstractHydrator { @@ -39,8 +44,8 @@ class ArrayHydrator extends AbstractHydrator private $_idTemplate = array(); private $_resultCounter = 0; - /** - * {@inheritdoc} + /** + * {@inheritdoc} */ protected function prepare() { @@ -49,7 +54,7 @@ class ArrayHydrator extends AbstractHydrator $this->_resultPointers = array(); $this->_idTemplate = array(); $this->_resultCounter = 0; - + foreach ($this->_rsm->aliasMap as $dqlAlias => $className) { $this->_identifierMap[$dqlAlias] = array(); $this->_resultPointers[$dqlAlias] = array(); @@ -57,14 +62,14 @@ class ArrayHydrator extends AbstractHydrator } } - /** - * {@inheritdoc} + /** + * {@inheritdoc} */ protected function hydrateAllData() { $result = array(); $cache = array(); - + while ($data = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { $this->hydrateRowData($data, $cache, $result); } @@ -85,8 +90,9 @@ class ArrayHydrator extends AbstractHydrator // Extract scalar values. They're appended at the end. if (isset($rowData['scalars'])) { $scalars = $rowData['scalars']; + unset($rowData['scalars']); - + if (empty($rowData)) { ++$this->_resultCounter; } @@ -100,7 +106,7 @@ class ArrayHydrator extends AbstractHydrator // It's a joined result $parent = $this->_rsm->parentAliasMap[$dqlAlias]; - $path = $parent . '.' . $dqlAlias; + $path = $parent . '.' . $dqlAlias; // missing parent data, skipping as RIGHT JOIN hydration is not supported. if ( ! isset($nonemptyComponents[$parent]) ) { @@ -119,22 +125,23 @@ class ArrayHydrator extends AbstractHydrator unset($this->_resultPointers[$dqlAlias]); // Ticket #1228 continue; } - + $relationAlias = $this->_rsm->relationMap[$dqlAlias]; $relation = $this->getClassMetadata($this->_rsm->aliasMap[$parent])->associationMappings[$relationAlias]; // Check the type of the relation (many or single-valued) if ( ! ($relation['type'] & ClassMetadata::TO_ONE)) { $oneToOne = false; + if (isset($nonemptyComponents[$dqlAlias])) { if ( ! isset($baseElement[$relationAlias])) { $baseElement[$relationAlias] = array(); } - - $indexExists = isset($this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]]); - $index = $indexExists ? $this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false; + + $indexExists = isset($this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]]); + $index = $indexExists ? $this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false; $indexIsValid = $index !== false ? isset($baseElement[$relationAlias][$index]) : false; - + if ( ! $indexExists || ! $indexIsValid) { $element = $data; if (isset($this->_rsm->indexByMap[$dqlAlias])) { @@ -142,15 +149,17 @@ class ArrayHydrator extends AbstractHydrator } else { $baseElement[$relationAlias][] = $element; } + end($baseElement[$relationAlias]); - $this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = - key($baseElement[$relationAlias]); + + $this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = key($baseElement[$relationAlias]); } } else if ( ! isset($baseElement[$relationAlias])) { $baseElement[$relationAlias] = array(); } } else { $oneToOne = true; + if ( ! isset($nonemptyComponents[$dqlAlias]) && ! isset($baseElement[$relationAlias])) { $baseElement[$relationAlias] = null; } else if ( ! isset($baseElement[$relationAlias])) { @@ -166,13 +175,14 @@ class ArrayHydrator extends AbstractHydrator } else { // It's a root result element - + $this->_rootAliases[$dqlAlias] = true; // Mark as root + $entityKey = $this->_rsm->entityMappings[$dqlAlias] ?: 0; // 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); + $result[] = array($entityKey => null); } else { $result[] = null; } @@ -180,12 +190,12 @@ class ArrayHydrator extends AbstractHydrator ++$this->_resultCounter; continue; } - + // Check for an existing element if ($this->_isSimpleQuery || ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { $element = $rowData[$dqlAlias]; if ($this->_rsm->isMixed) { - $element = array(0 => $element); + $element = array($entityKey => $element); } if (isset($this->_rsm->indexByMap[$dqlAlias])) { @@ -240,37 +250,37 @@ class ArrayHydrator extends AbstractHydrator { if ($coll === null) { unset($this->_resultPointers[$dqlAlias]); // Ticket #1228 - + return; } - + if ($index !== false) { $this->_resultPointers[$dqlAlias] =& $coll[$index]; - + return; - } - + } + if ( ! $coll) { return; } - + if ($oneToOne) { $this->_resultPointers[$dqlAlias] =& $coll; - + return; } - + end($coll); $this->_resultPointers[$dqlAlias] =& $coll[key($coll)]; - + return; } - + /** * Retrieve ClassMetadata associated to entity class name. - * + * * @param string $className - * + * * @return Doctrine\ORM\Mapping\ClassMetadata */ private function getClassMetadata($className) @@ -278,7 +288,7 @@ class ArrayHydrator extends AbstractHydrator if ( ! isset($this->_ce[$className])) { $this->_ce[$className] = $this->_em->getClassMetadata($className); } - + return $this->_ce[$className]; } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index d7d0281c4..c56b6eb22 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -32,8 +32,13 @@ use PDO, * @since 2.0 * @author Roman Borschel * @author Guilherme Blanco - * + * * @internal Highly performance-sensitive code. + * + * @todo General behavior is "wrong" if you define an alias to selected IdentificationVariable. + * Example: SELECT u AS user FROM User u + * The result should contains an array where each array index is an array: array('user' => [User object]) + * Problem must be solved somehow by removing the isMixed in ResultSetMapping */ class ObjectHydrator extends AbstractHydrator { @@ -60,52 +65,67 @@ class ObjectHydrator extends AbstractHydrator $this->_identifierMap = $this->_resultPointers = $this->_idTemplate = array(); + $this->_resultCounter = 0; - if (!isset($this->_hints['deferEagerLoad'])) { + + if ( ! isset($this->_hints['deferEagerLoad'])) { $this->_hints['deferEagerLoad'] = true; } foreach ($this->_rsm->aliasMap as $dqlAlias => $className) { $this->_identifierMap[$dqlAlias] = array(); - $this->_idTemplate[$dqlAlias] = ''; - $class = $this->_em->getClassMetadata($className); + $this->_idTemplate[$dqlAlias] = ''; if ( ! isset($this->_ce[$className])) { - $this->_ce[$className] = $class; + $this->_ce[$className] = $this->_em->getClassMetadata($className); } // Remember which associations are "fetch joined", so that we know where to inject // collection stubs or proxies and where not. - if (isset($this->_rsm->relationMap[$dqlAlias])) { - if ( ! isset($this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]])) { - throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $this->_rsm->parentAliasMap[$dqlAlias]); + if ( ! isset($this->_rsm->relationMap[$dqlAlias])) { + continue; + } + + if ( ! isset($this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]])) { + throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $this->_rsm->parentAliasMap[$dqlAlias]); + } + + $sourceClassName = $this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]]; + $sourceClass = $this->_getClassMetadata($sourceClassName); + $assoc = $sourceClass->associationMappings[$this->_rsm->relationMap[$dqlAlias]]; + + $this->_hints['fetched'][$sourceClassName][$assoc['fieldName']] = true; + + if ($sourceClass->subClasses) { + foreach ($sourceClass->subClasses as $sourceSubclassName) { + $this->_hints['fetched'][$sourceSubclassName][$assoc['fieldName']] = true; + } + } + + if ($assoc['type'] === ClassMetadata::MANY_TO_MANY) { + continue; + } + + // Mark any non-collection opposite sides as fetched, too. + if ($assoc['mappedBy']) { + $this->_hints['fetched'][$className][$assoc['mappedBy']] = true; + + continue; + } + + if ($assoc['inversedBy']) { + $class = $this->_ce[$className]; + $inverseAssoc = $class->associationMappings[$assoc['inversedBy']]; + + if ( ! ($inverseAssoc['type'] & ClassMetadata::TO_ONE)) { + continue; } - $sourceClassName = $this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]]; - $sourceClass = $this->_getClassMetadata($sourceClassName); - $assoc = $sourceClass->associationMappings[$this->_rsm->relationMap[$dqlAlias]]; - $this->_hints['fetched'][$sourceClassName][$assoc['fieldName']] = true; - if ($sourceClass->subClasses) { - foreach ($sourceClass->subClasses as $sourceSubclassName) { - $this->_hints['fetched'][$sourceSubclassName][$assoc['fieldName']] = true; - } - } - if ($assoc['type'] != ClassMetadata::MANY_TO_MANY) { - // Mark any non-collection opposite sides as fetched, too. - if ($assoc['mappedBy']) { - $this->_hints['fetched'][$className][$assoc['mappedBy']] = true; - } else { - if ($assoc['inversedBy']) { - $inverseAssoc = $class->associationMappings[$assoc['inversedBy']]; - if ($inverseAssoc['type'] & ClassMetadata::TO_ONE) { - $this->_hints['fetched'][$className][$inverseAssoc['fieldName']] = true; - if ($class->subClasses) { - foreach ($class->subClasses as $targetSubclassName) { - $this->_hints['fetched'][$targetSubclassName][$inverseAssoc['fieldName']] = true; - } - } - } - } + $this->_hints['fetched'][$className][$inverseAssoc['fieldName']] = true; + + if ($class->subClasses) { + foreach ($class->subClasses as $targetSubclassName) { + $this->_hints['fetched'][$targetSubclassName][$inverseAssoc['fieldName']] = true; } } } @@ -120,7 +140,7 @@ class ObjectHydrator extends AbstractHydrator $eagerLoad = (isset($this->_hints['deferEagerLoad'])) && $this->_hints['deferEagerLoad'] == true; parent::cleanup(); - + $this->_identifierMap = $this->_initializedCollections = $this->_existingCollections = @@ -137,7 +157,7 @@ class ObjectHydrator extends AbstractHydrator protected function hydrateAllData() { $result = array(); - $cache = array(); + $cache = array(); while ($row = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { $this->hydrateRowData($row, $cache, $result); @@ -159,31 +179,34 @@ class ObjectHydrator extends AbstractHydrator */ private function _initRelatedCollection($entity, $class, $fieldName) { - $oid = spl_object_hash($entity); + $oid = spl_object_hash($entity); $relation = $class->associationMappings[$fieldName]; + $value = $class->reflFields[$fieldName]->getValue($entity); - $value = $class->reflFields[$fieldName]->getValue($entity); if ($value === null) { $value = new ArrayCollection; } if ( ! $value instanceof PersistentCollection) { $value = new PersistentCollection( - $this->_em, - $this->_ce[$relation['targetEntity']], - $value + $this->_em, $this->_ce[$relation['targetEntity']], $value ); $value->setOwner($entity, $relation); + $class->reflFields[$fieldName]->setValue($entity, $value); $this->_uow->setOriginalEntityProperty($oid, $fieldName, $value); + $this->_initializedCollections[$oid . $fieldName] = $value; - } else if (isset($this->_hints[Query::HINT_REFRESH]) || - isset($this->_hints['fetched'][$class->name][$fieldName]) && - ! $value->isInitialized()) { + } else if ( + isset($this->_hints[Query::HINT_REFRESH]) || + isset($this->_hints['fetched'][$class->name][$fieldName]) && + ! $value->isInitialized() + ) { // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED! $value->setDirty(false); $value->setInitialized(true); $value->unwrap()->clear(); + $this->_initializedCollections[$oid . $fieldName] = $value; } else { // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN! @@ -203,6 +226,7 @@ class ObjectHydrator extends AbstractHydrator private function _getEntity(array $data, $dqlAlias) { $className = $this->_rsm->aliasMap[$dqlAlias]; + if (isset($this->_rsm->discriminatorColumns[$dqlAlias])) { $discrColumn = $this->_rsm->metaMappings[$this->_rsm->discriminatorColumns[$dqlAlias]]; @@ -211,12 +235,12 @@ class ObjectHydrator extends AbstractHydrator } $className = $this->_ce[$className]->discriminatorMap[$data[$discrColumn]]; + unset($data[$discrColumn]); } if (isset($this->_hints[Query::HINT_REFRESH_ENTITY]) && isset($this->_rootAliases[$dqlAlias])) { - $class = $this->_ce[$className]; - $this->registerManaged($class, $this->_hints[Query::HINT_REFRESH_ENTITY], $data); + $this->registerManaged($this->_ce[$className], $this->_hints[Query::HINT_REFRESH_ENTITY], $data); } return $this->_uow->createEntity($className, $data, $this->_hints); @@ -226,6 +250,7 @@ class ObjectHydrator extends AbstractHydrator { // TODO: Abstract this code and UnitOfWork::createEntity() equivalent? $class = $this->_ce[$className]; + /* @var $class ClassMetadata */ if ($class->isIdentifierComposite) { $idHash = ''; @@ -257,6 +282,7 @@ class ObjectHydrator extends AbstractHydrator if ( ! isset($this->_ce[$className])) { $this->_ce[$className] = $this->_em->getClassMetadata($className); } + return $this->_ce[$className]; } @@ -387,6 +413,7 @@ class ObjectHydrator extends AbstractHydrator $reflField->setValue($parentObject, $element); $this->_uow->setOriginalEntityProperty($oid, $relationField, $element); $targetClass = $this->_ce[$relation['targetEntity']]; + if ($relation['isOwningSide']) { //TODO: Just check hints['fetched'] here? // If there is an inverse mapping on the target class its bidirectional @@ -417,11 +444,12 @@ class ObjectHydrator extends AbstractHydrator } else { // PATH C: Its a root result element $this->_rootAliases[$dqlAlias] = true; // Mark as root alias + $entityKey = $this->_rsm->entityMappings[$dqlAlias] ?: 0; // 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); + $result[] = array($entityKey => null); } else { $result[] = null; } @@ -434,7 +462,7 @@ class ObjectHydrator extends AbstractHydrator if ( ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { $element = $this->_getEntity($rowData[$dqlAlias], $dqlAlias); if ($this->_rsm->isMixed) { - $element = array(0 => $element); + $element = array($entityKey => $element); } if (isset($this->_rsm->indexByMap[$dqlAlias])) { diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 9fc30bb8c..2af4bb452 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -141,9 +141,9 @@ class Parser */ public function __construct(Query $query) { - $this->_query = $query; - $this->_em = $query->getEntityManager(); - $this->_lexer = new Lexer($query->getDql()); + $this->_query = $query; + $this->_em = $query->getEntityManager(); + $this->_lexer = new Lexer($query->getDql()); $this->_parserResult = new ParserResult(); } @@ -226,6 +226,11 @@ class Parser $this->_processDeferredResultVariables(); } + $this->_processRootEntityAliasSelected(); + + // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot! + $this->fixIdentificationVariableOrder($AST); + return $AST; } @@ -241,11 +246,10 @@ class Parser */ public function match($token) { + $lookaheadType = $this->_lexer->lookahead['type']; + // short-circuit on first condition, usually types match - if ($this->_lexer->lookahead['type'] !== $token && - $token !== Lexer::T_IDENTIFIER && - $this->_lexer->lookahead['type'] <= Lexer::T_IDENTIFIER - ) { + if ($lookaheadType !== $token && $token !== Lexer::T_IDENTIFIER && $lookaheadType <= Lexer::T_IDENTIFIER) { $this->syntaxError($this->_lexer->getLiteral($token)); } @@ -281,9 +285,6 @@ class Parser { $AST = $this->getAST(); - $this->fixIdentificationVariableOrder($AST); - $this->assertSelectEntityRootAliasRequirement(); - if (($customWalkers = $this->_query->getHint(Query::HINT_CUSTOM_TREE_WALKERS)) !== false) { $this->_customTreeWalkers = $customWalkers; } @@ -300,68 +301,57 @@ class Parser $treeWalkerChain->addTreeWalker($walker); } - if ($AST instanceof AST\SelectStatement) { - $treeWalkerChain->walkSelectStatement($AST); - } else if ($AST instanceof AST\UpdateStatement) { - $treeWalkerChain->walkUpdateStatement($AST); - } else { - $treeWalkerChain->walkDeleteStatement($AST); + switch (true) { + case ($AST instanceof AST\UpdateStatement): + $treeWalkerChain->walkUpdateStatement($AST); + break; + + case ($AST instanceof AST\DeleteStatement): + $treeWalkerChain->walkDeleteStatement($AST); + break; + + case ($AST instanceof AST\SelectStatement): + default: + $treeWalkerChain->walkSelectStatement($AST); } } - if ($this->_customOutputWalker) { - $outputWalker = new $this->_customOutputWalker( - $this->_query, $this->_parserResult, $this->_queryComponents - ); - } else { - $outputWalker = new SqlWalker( - $this->_query, $this->_parserResult, $this->_queryComponents - ); - } + $outputWalkerClass = $this->_customOutputWalker ?: __NAMESPACE__ . '\SqlWalker'; + $outputWalker = new $outputWalkerClass($this->_query, $this->_parserResult, $this->_queryComponents); // Assign an SQL executor to the parser result $this->_parserResult->setSqlExecutor($outputWalker->getExecutor($AST)); return $this->_parserResult; } - - private function assertSelectEntityRootAliasRequirement() - { - if ( count($this->_identVariableExpressions) > 0) { - $foundRootEntity = false; - foreach ($this->_identVariableExpressions AS $dqlAlias => $expr) { - if (isset($this->_queryComponents[$dqlAlias]) && $this->_queryComponents[$dqlAlias]['parent'] === null) { - $foundRootEntity = true; - } - } - - if (!$foundRootEntity) { - $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.'); - } - } - } - + /** * Fix order of identification variables. - * + * * They have to appear in the select clause in the same order as the * declarations (from ... x join ... y join ... z ...) appear in the query * as the hydration process relies on that order for proper operation. - * + * * @param AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST * @return void */ private function fixIdentificationVariableOrder($AST) { - if ( count($this->_identVariableExpressions) > 1) { - foreach ($this->_queryComponents as $dqlAlias => $qComp) { - if (isset($this->_identVariableExpressions[$dqlAlias])) { - $expr = $this->_identVariableExpressions[$dqlAlias]; - $key = array_search($expr, $AST->selectClause->selectExpressions); - unset($AST->selectClause->selectExpressions[$key]); - $AST->selectClause->selectExpressions[] = $expr; - } + if (count($this->_identVariableExpressions) <= 1) { + return; + } + + foreach ($this->_queryComponents as $dqlAlias => $qComp) { + if ( ! isset($this->_identVariableExpressions[$dqlAlias])) { + continue; } + + $expr = $this->_identVariableExpressions[$dqlAlias]; + $key = array_search($expr, $AST->selectClause->selectExpressions); + + unset($AST->selectClause->selectExpressions[$key]); + + $AST->selectClause->selectExpressions[] = $expr; } } @@ -380,19 +370,10 @@ class Parser } $tokenPos = (isset($token['position'])) ? $token['position'] : '-1'; + $message = "line 0, col {$tokenPos}: Error: "; - - if ($expected !== '') { - $message .= "Expected {$expected}, got "; - } else { - $message .= 'Unexpected '; - } - - if ($this->_lexer->lookahead === null) { - $message .= 'end of string.'; - } else { - $message .= "'{$token['value']}'"; - } + $message .= ($expected !== '') ? "Expected {$expected}, got " : 'Unexpected '; + $message .= ($this->_lexer->lookahead === null) ? 'end of string.' : "'{$token['value']}'"; throw QueryException::syntaxError($message); } @@ -415,18 +396,19 @@ class Parser $distance = 12; // Find a position of a final word to display in error string - $dql = $this->_query->getDql(); + $dql = $this->_query->getDql(); $length = strlen($dql); - $pos = $token['position'] + $distance; - $pos = strpos($dql, ' ', ($length > $pos) ? $pos : $length); + $pos = $token['position'] + $distance; + $pos = strpos($dql, ' ', ($length > $pos) ? $pos : $length); $length = ($pos !== false) ? $pos - $token['position'] : $distance; - // Building informative message - $message = 'line 0, col ' . ( - (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1' - ) . " near '" . substr($dql, $token['position'], $length) . "': Error: " . $message; + $tokenPos = (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1'; + $tokenStr = substr($dql, $token['position'], $length); - throw \Doctrine\ORM\Query\QueryException::semanticalError($message); + // Building informative message + $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message; + + throw QueryException::semanticalError($message); } /** @@ -460,15 +442,22 @@ class Parser $numUnmatched = 1; while ($numUnmatched > 0 && $token !== null) { - if ($token['value'] == ')') { - --$numUnmatched; - } else if ($token['value'] == '(') { - ++$numUnmatched; + switch ($token['type']) { + case Lexer::T_OPEN_PARENTHESIS: + ++$numUnmatched; + break; + + case Lexer::T_CLOSE_PARENTHESIS: + --$numUnmatched; + break; + + default: + // Do nothing } $token = $this->_lexer->peek(); } - + $this->_lexer->resetPeek(); return $token; @@ -481,7 +470,7 @@ class Parser */ private function _isMathOperator($token) { - return in_array($token['value'], array("+", "-", "/", "*")); + return in_array($token['type'], array(Lexer::T_PLUS, Lexer::T_MINUS, Lexer::T_DIVIDE, Lexer::T_MULTIPLY)); } /** @@ -491,12 +480,13 @@ class Parser */ private function _isFunction() { - $peek = $this->_lexer->peek(); + $peek = $this->_lexer->peek(); $nextpeek = $this->_lexer->peek(); + $this->_lexer->resetPeek(); // We deny the COUNT(SELECT * FROM User u) here. COUNT won't be considered a function - return ($peek['value'] === '(' && $nextpeek['type'] !== Lexer::T_SELECT); + return ($peek['type'] === Lexer::T_OPEN_PARENTHESIS && $nextpeek['type'] !== Lexer::T_SELECT); } /** @@ -506,35 +496,17 @@ class Parser */ private function _isAggregateFunction($tokenType) { - return $tokenType == Lexer::T_AVG || $tokenType == Lexer::T_MIN || - $tokenType == Lexer::T_MAX || $tokenType == Lexer::T_SUM || - $tokenType == Lexer::T_COUNT; + return in_array($tokenType, array(Lexer::T_AVG, Lexer::T_MIN, Lexer::T_MAX, Lexer::T_SUM, Lexer::T_COUNT)); } /** - * Checks whether the current lookahead token of the lexer has the type - * T_ALL, T_ANY or T_SOME. + * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME. * * @return boolean */ private function _isNextAllAnySome() { - return $this->_lexer->lookahead['type'] === Lexer::T_ALL || - $this->_lexer->lookahead['type'] === Lexer::T_ANY || - $this->_lexer->lookahead['type'] === Lexer::T_SOME; - } - - /** - * Checks whether the next 2 tokens start a subselect. - * - * @return boolean TRUE if the next 2 tokens start a subselect, FALSE otherwise. - */ - private function _isSubselect() - { - $la = $this->_lexer->lookahead; - $next = $this->_lexer->glimpse(); - - return ($la['value'] === '(' && $next['type'] === Lexer::T_SELECT); + return in_array($this->_lexer->lookahead['type'], array(Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME)); } /** @@ -586,12 +558,13 @@ class Parser $class = $this->_queryComponents[$expr->identificationVariable]['metadata']; foreach ($expr->partialFieldSet as $field) { - if ( ! isset($class->fieldMappings[$field])) { - $this->semanticalError( - "There is no mapped field named '$field' on class " . $class->name . ".", - $deferredItem['token'] - ); + if (isset($class->fieldMappings[$field])) { + continue; } + + $this->semanticalError( + "There is no mapped field named '$field' on class " . $class->name . ".", $deferredItem['token'] + ); } if (array_intersect($class->identifier, $expr->partialFieldSet) != $class->identifier) { @@ -662,7 +635,7 @@ class Parser if (($field = $pathExpression->field) === null) { $field = $pathExpression->field = $class->identifier[0]; } - + // Check if field or association exists if ( ! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) { $this->semanticalError( @@ -671,17 +644,14 @@ class Parser ); } - if (isset($class->fieldMappings[$field])) { - $fieldType = AST\PathExpression::TYPE_STATE_FIELD; - } else { - $assoc = $class->associationMappings[$field]; - $class = $this->_em->getClassMetadata($assoc['targetEntity']); + $fieldType = AST\PathExpression::TYPE_STATE_FIELD; - if ($assoc['type'] & ClassMetadata::TO_ONE) { - $fieldType = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; - } else { - $fieldType = AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION; - } + if (isset($class->associationMappings[$field])) { + $assoc = $class->associationMappings[$field]; + + $fieldType = ($assoc['type'] & ClassMetadata::TO_ONE) + ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION + : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION; } // Validate if PathExpression is one of the expected types @@ -717,12 +687,31 @@ class Parser $this->semanticalError($semanticalError, $deferredItem['token']); } - + // We need to force the type in PathExpression $pathExpression->type = $fieldType; } } + private function _processRootEntityAliasSelected() + { + if ( ! count($this->_identVariableExpressions)) { + return; + } + + $foundRootEntity = false; + + foreach ($this->_identVariableExpressions AS $dqlAlias => $expr) { + if (isset($this->_queryComponents[$dqlAlias]) && $this->_queryComponents[$dqlAlias]['parent'] === null) { + $foundRootEntity = true; + } + } + + if ( ! $foundRootEntity) { + $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.'); + } + } + /** * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement * @@ -738,12 +727,15 @@ class Parser case Lexer::T_SELECT: $statement = $this->SelectStatement(); break; + case Lexer::T_UPDATE: $statement = $this->UpdateStatement(); break; + case Lexer::T_DELETE: $statement = $this->DeleteStatement(); break; + default: $this->syntaxError('SELECT, UPDATE or DELETE'); break; @@ -766,17 +758,10 @@ class Parser { $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause()); - $selectStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) - ? $this->WhereClause() : null; - - $selectStatement->groupByClause = $this->_lexer->isNextToken(Lexer::T_GROUP) - ? $this->GroupByClause() : null; - - $selectStatement->havingClause = $this->_lexer->isNextToken(Lexer::T_HAVING) - ? $this->HavingClause() : null; - - $selectStatement->orderByClause = $this->_lexer->isNextToken(Lexer::T_ORDER) - ? $this->OrderByClause() : null; + $selectStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null; + $selectStatement->groupByClause = $this->_lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null; + $selectStatement->havingClause = $this->_lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null; + $selectStatement->orderByClause = $this->_lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null; return $selectStatement; } @@ -789,8 +774,8 @@ class Parser public function UpdateStatement() { $updateStatement = new AST\UpdateStatement($this->UpdateClause()); - $updateStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) - ? $this->WhereClause() : null; + + $updateStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null; return $updateStatement; } @@ -803,8 +788,8 @@ class Parser public function DeleteStatement() { $deleteStatement = new AST\DeleteStatement($this->DeleteClause()); - $deleteStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) - ? $this->WhereClause() : null; + + $deleteStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null; return $deleteStatement; } @@ -842,9 +827,7 @@ class Parser $exists = isset($this->_queryComponents[$aliasIdentVariable]); if ($exists) { - $this->semanticalError( - "'$aliasIdentVariable' is already defined.", $this->_lexer->token - ); + $this->semanticalError("'$aliasIdentVariable' is already defined.", $this->_lexer->token); } return $aliasIdentVariable; @@ -863,6 +846,7 @@ class Parser if (strrpos($schemaName, ':') !== false) { list($namespaceAlias, $simpleClassName) = explode(':', $schemaName); + $schemaName = $this->_em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName; } @@ -888,9 +872,7 @@ class Parser $exists = isset($this->_queryComponents[$resultVariable]); if ($exists) { - $this->semanticalError( - "'$resultVariable' is already defined.", $this->_lexer->token - ); + $this->semanticalError("'$resultVariable' is already defined.", $this->_lexer->token); } return $resultVariable; @@ -924,11 +906,13 @@ class Parser */ public function JoinAssociationPathExpression() { - $token = $this->_lexer->lookahead; + $token = $this->_lexer->lookahead; $identVariable = $this->IdentificationVariable(); - if (!isset($this->_queryComponents[$identVariable])) { - $this->semanticalError('Identification Variable ' . $identVariable .' used in join path expression but was not defined before.'); + if ( ! isset($this->_queryComponents[$identVariable])) { + $this->semanticalError( + 'Identification Variable ' . $identVariable .' used in join path expression but was not defined before.' + ); } $this->match(Lexer::T_DOT); @@ -968,7 +952,7 @@ class Parser $field = $this->_lexer->token['value']; } - + // Creating AST node $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field); @@ -1051,6 +1035,7 @@ class Parser // Check for DISTINCT if ($this->_lexer->isNextToken(Lexer::T_DISTINCT)) { $this->match(Lexer::T_DISTINCT); + $isDistinct = true; } @@ -1060,6 +1045,7 @@ class Parser while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); + $selectExpressions[] = $this->SelectExpression(); } @@ -1078,6 +1064,7 @@ class Parser if ($this->_lexer->isNextToken(Lexer::T_DISTINCT)) { $this->match(Lexer::T_DISTINCT); + $isDistinct = true; } @@ -1112,6 +1099,7 @@ class Parser 'nestingLevel' => $this->_nestingLevel, 'token' => $token, ); + $this->_queryComponents[$aliasIdentificationVariable] = $queryComponent; $this->match(Lexer::T_SET); @@ -1121,6 +1109,7 @@ class Parser while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); + $updateItems[] = $this->UpdateItem(); } @@ -1164,6 +1153,7 @@ class Parser 'nestingLevel' => $this->_nestingLevel, 'token' => $token, ); + $this->_queryComponents[$aliasIdentificationVariable] = $queryComponent; return $deleteClause; @@ -1177,11 +1167,13 @@ class Parser public function FromClause() { $this->match(Lexer::T_FROM); + $identificationVariableDeclarations = array(); $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); + $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); } @@ -1196,11 +1188,13 @@ class Parser public function SubselectFromClause() { $this->match(Lexer::T_FROM); + $identificationVariables = array(); $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); + $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); } @@ -1245,6 +1239,7 @@ class Parser while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); + $groupByItems[] = $this->GroupByItem(); } @@ -1266,6 +1261,7 @@ class Parser while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); + $orderByItems[] = $this->OrderByItem(); } @@ -1284,17 +1280,10 @@ class Parser $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause()); - $subselect->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) - ? $this->WhereClause() : null; - - $subselect->groupByClause = $this->_lexer->isNextToken(Lexer::T_GROUP) - ? $this->GroupByClause() : null; - - $subselect->havingClause = $this->_lexer->isNextToken(Lexer::T_HAVING) - ? $this->HavingClause() : null; - - $subselect->orderByClause = $this->_lexer->isNextToken(Lexer::T_ORDER) - ? $this->OrderByClause() : null; + $subselect->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null; + $subselect->groupByClause = $this->_lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null; + $subselect->havingClause = $this->_lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null; + $subselect->orderByClause = $this->_lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null; // Decrease query nesting level $this->_nestingLevel--; @@ -1331,11 +1320,11 @@ class Parser if ($glimpse['type'] == Lexer::T_DOT) { return $this->SingleValuedPathExpression(); } - + $token = $this->_lexer->lookahead; $identVariable = $this->IdentificationVariable(); - if (!isset($this->_queryComponents[$identVariable])) { + if ( ! isset($this->_queryComponents[$identVariable])) { $this->semanticalError('Cannot group by undefined identification variable.'); } @@ -1353,21 +1342,26 @@ class Parser // We need to check if we are in a ResultVariable or StateFieldPathExpression $glimpse = $this->_lexer->glimpse(); - - $expr = ($glimpse['type'] != Lexer::T_DOT) - ? $this->ResultVariable() - : $this->SingleValuedPathExpression(); + $expr = ($glimpse['type'] != Lexer::T_DOT) ? $this->ResultVariable() : $this->SingleValuedPathExpression(); $item = new AST\OrderByItem($expr); - if ($this->_lexer->isNextToken(Lexer::T_ASC)) { - $this->match(Lexer::T_ASC); - } else if ($this->_lexer->isNextToken(Lexer::T_DESC)) { - $this->match(Lexer::T_DESC); - $type = 'DESC'; + switch (true) { + case ($this->_lexer->isNextToken(Lexer::T_DESC)): + $this->match(Lexer::T_DESC); + $type = 'DESC'; + break; + + case ($this->_lexer->isNextToken(Lexer::T_ASC)): + $this->match(Lexer::T_ASC); + break; + + default: + // Do nothing } $item->type = $type; + return $item; } @@ -1386,9 +1380,13 @@ class Parser { if ($this->_lexer->isNextToken(Lexer::T_NULL)) { $this->match(Lexer::T_NULL); + return null; - } else if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { + } + + if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { $this->match(Lexer::T_INPUT_PARAMETER); + return new AST\InputParameter($this->_lexer->token['value']); } @@ -1451,9 +1449,8 @@ class Parser */ public function JoinVariableDeclaration() { - $join = $this->Join(); - $indexBy = $this->_lexer->isNextToken(Lexer::T_INDEX) - ? $this->IndexBy() : null; + $join = $this->Join(); + $indexBy = $this->_lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null; return new AST\JoinVariableDeclaration($join, $indexBy); } @@ -1484,6 +1481,7 @@ class Parser 'nestingLevel' => $this->_nestingLevel, 'token' => $token ); + $this->_queryComponents[$aliasIdentificationVariable] = $queryComponent; return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable); @@ -1502,18 +1500,20 @@ class Parser $partialFieldSet = array(); $identificationVariable = $this->IdentificationVariable(); - $this->match(Lexer::T_DOT); + $this->match(Lexer::T_DOT); $this->match(Lexer::T_OPEN_CURLY_BRACE); $this->match(Lexer::T_IDENTIFIER); + $partialFieldSet[] = $this->_lexer->token['value']; while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); $this->match(Lexer::T_IDENTIFIER); + $partialFieldSet[] = $this->_lexer->token['value']; } - + $this->match(Lexer::T_CLOSE_CURLY_BRACE); $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet); @@ -1539,18 +1539,26 @@ class Parser // Check Join type $joinType = AST\Join::JOIN_TYPE_INNER; - if ($this->_lexer->isNextToken(Lexer::T_LEFT)) { - $this->match(Lexer::T_LEFT); + switch (true) { + case ($this->_lexer->isNextToken(Lexer::T_LEFT)): + $this->match(Lexer::T_LEFT); - // Possible LEFT OUTER join - if ($this->_lexer->isNextToken(Lexer::T_OUTER)) { - $this->match(Lexer::T_OUTER); - $joinType = AST\Join::JOIN_TYPE_LEFTOUTER; - } else { $joinType = AST\Join::JOIN_TYPE_LEFT; - } - } else if ($this->_lexer->isNextToken(Lexer::T_INNER)) { - $this->match(Lexer::T_INNER); + + // Possible LEFT OUTER join + if ($this->_lexer->isNextToken(Lexer::T_OUTER)) { + $this->match(Lexer::T_OUTER); + + $joinType = AST\Join::JOIN_TYPE_LEFTOUTER; + } + break; + + case ($this->_lexer->isNextToken(Lexer::T_INNER)): + $this->match(Lexer::T_INNER); + break; + + default: + // Do nothing } $this->match(Lexer::T_JOIN); @@ -1566,7 +1574,7 @@ class Parser // Verify that the association exists. $parentClass = $this->_queryComponents[$joinPathExpression->identificationVariable]['metadata']; - $assocField = $joinPathExpression->associationField; + $assocField = $joinPathExpression->associationField; if ( ! $parentClass->hasAssociation($assocField)) { $this->semanticalError( @@ -1585,6 +1593,7 @@ class Parser 'nestingLevel' => $this->_nestingLevel, 'token' => $token ); + $this->_queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; // Create AST node @@ -1593,6 +1602,7 @@ class Parser // Check for ad-hoc Join conditions if ($this->_lexer->isNextToken(Lexer::T_WITH)) { $this->match(Lexer::T_WITH); + $join->conditionalExpression = $this->ConditionalExpression(); } @@ -1626,180 +1636,198 @@ class Parser public function ScalarExpression() { $lookahead = $this->_lexer->lookahead['type']; - if ($lookahead === Lexer::T_IDENTIFIER) { - $this->_lexer->peek(); // lookahead => '.' - $this->_lexer->peek(); // lookahead => token after '.' - $peek = $this->_lexer->peek(); // lookahead => token after the token after the '.' - $this->_lexer->resetPeek(); - if ($this->_isMathOperator($peek)) { + switch ($lookahead) { + case Lexer::T_IDENTIFIER: + $this->_lexer->peek(); // lookahead => '.' + $this->_lexer->peek(); // lookahead => token after '.' + $peek = $this->_lexer->peek(); // lookahead => token after the token after the '.' + $this->_lexer->resetPeek(); + + if ($this->_isMathOperator($peek)) { + return $this->SimpleArithmeticExpression(); + } + + return $this->StateFieldPathExpression(); + + case Lexer::T_INTEGER: + case Lexer::T_FLOAT: return $this->SimpleArithmeticExpression(); - } - return $this->StateFieldPathExpression(); - } else if ($lookahead == Lexer::T_INTEGER || $lookahead == Lexer::T_FLOAT) { - return $this->SimpleArithmeticExpression(); - } else if ($lookahead == Lexer::T_CASE || $lookahead == Lexer::T_COALESCE || $lookahead == Lexer::T_NULLIF) { - // Since NULLIF and COALESCE can be identified as a function, - // we need to check if before check for FunctionDeclaration - return $this->CaseExpression(); - } else if ($this->_isFunction() || $this->_isAggregateFunction($this->_lexer->lookahead['type'])) { - // We may be in an ArithmeticExpression (find the matching ")" and inspect for Math operator) - $this->_lexer->peek(); // "(" - $peek = $this->_peekBeyondClosingParenthesis(); + case Lexer::T_STRING: + return $this->StringPrimary(); - if ($this->_isMathOperator($peek)) { - return $this->SimpleArithmeticExpression(); - } + case Lexer::T_TRUE: + case Lexer::T_FALSE: + $this->match($lookahead); - if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { - return $this->AggregateExpression(); - } - - return $this->FunctionDeclaration(); - } else if ($lookahead == Lexer::T_STRING) { - return $this->StringPrimary(); - } else if ($lookahead == Lexer::T_INPUT_PARAMETER) { - return $this->InputParameter(); - } else if ($lookahead == Lexer::T_TRUE || $lookahead == Lexer::T_FALSE) { - $this->match($lookahead); - return new AST\Literal(AST\Literal::BOOLEAN, $this->_lexer->token['value']); - } else { - $this->syntaxError(); + return new AST\Literal(AST\Literal::BOOLEAN, $this->_lexer->token['value']); + + case Lexer::T_INPUT_PARAMETER: + return $this->InputParameter(); + + case Lexer::T_CASE: + case Lexer::T_COALESCE: + case Lexer::T_NULLIF: + // Since NULLIF and COALESCE can be identified as a function, + // we need to check if before check for FunctionDeclaration + return $this->CaseExpression(); + + default: + if ( ! ($this->_isFunction() || $this->_isAggregateFunction($lookahead))) { + $this->syntaxError(); + } + + // We may be in an ArithmeticExpression (find the matching ")" and inspect for Math operator) + $this->_lexer->peek(); // "(" + $peek = $this->_peekBeyondClosingParenthesis(); + + if ($this->_isMathOperator($peek)) { + return $this->SimpleArithmeticExpression(); + } + + if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { + return $this->AggregateExpression(); + } + + return $this->FunctionDeclaration(); } } /** - * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression - * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" - * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression - * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" - * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator - * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression - * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" + * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression + * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" + * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression + * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" + * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator + * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression + * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" - * + * * @return mixed One of the possible expressions or subexpressions. */ public function CaseExpression() { $lookahead = $this->_lexer->lookahead['type']; - + switch ($lookahead) { case Lexer::T_NULLIF: return $this->NullIfExpression(); - + case Lexer::T_COALESCE: return $this->CoalesceExpression(); - + case Lexer::T_CASE: $this->_lexer->resetPeek(); $peek = $this->_lexer->peek(); - - return ($peek['type'] === Lexer::T_WHEN) - ? $this->GeneralCaseExpression() - : $this->SimpleCaseExpression(); - + + if ($peek['type'] === Lexer::T_WHEN) { + return $this->GeneralCaseExpression(); + } + + return $this->SimpleCaseExpression(); + default: // Do nothing break; } - + $this->syntaxError(); } - + /** * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" - * - * @return Doctrine\ORM\Query\AST\CoalesceExpression + * + * @return Doctrine\ORM\Query\AST\CoalesceExpression */ public function CoalesceExpression() { $this->match(Lexer::T_COALESCE); $this->match(Lexer::T_OPEN_PARENTHESIS); - + // Process ScalarExpressions (1..N) $scalarExpressions = array(); $scalarExpressions[] = $this->ScalarExpression(); while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); + $scalarExpressions[] = $this->ScalarExpression(); } - + $this->match(Lexer::T_CLOSE_PARENTHESIS); - + return new AST\CoalesceExpression($scalarExpressions); } - + /** * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" - * - * @return Doctrine\ORM\Query\AST\NullIfExpression + * + * @return Doctrine\ORM\Query\AST\NullIfExpression */ public function NullIfExpression() { $this->match(Lexer::T_NULLIF); $this->match(Lexer::T_OPEN_PARENTHESIS); - + $firstExpression = $this->ScalarExpression(); $this->match(Lexer::T_COMMA); $secondExpression = $this->ScalarExpression(); - + $this->match(Lexer::T_CLOSE_PARENTHESIS); return new AST\NullIfExpression($firstExpression, $secondExpression); } - + /** - * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" - * - * @return Doctrine\ORM\Query\AST\GeneralExpression + * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" + * + * @return Doctrine\ORM\Query\AST\GeneralExpression */ public function GeneralCaseExpression() { $this->match(Lexer::T_CASE); - + // Process WhenClause (1..N) $whenClauses = array(); - + do { $whenClauses[] = $this->WhenClause(); } while ($this->_lexer->isNextToken(Lexer::T_WHEN)); - + $this->match(Lexer::T_ELSE); $scalarExpression = $this->ScalarExpression(); $this->match(Lexer::T_END); - + return new AST\GeneralCaseExpression($whenClauses, $scalarExpression); } - + /** - * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" - * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator + * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" + * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator */ public function SimpleCaseExpression() { $this->match(Lexer::T_CASE); $caseOperand = $this->StateFieldPathExpression(); - + // Process SimpleWhenClause (1..N) $simpleWhenClauses = array(); - + do { $simpleWhenClauses[] = $this->SimpleWhenClause(); } while ($this->_lexer->isNextToken(Lexer::T_WHEN)); - + $this->match(Lexer::T_ELSE); $scalarExpression = $this->ScalarExpression(); $this->match(Lexer::T_END); - + return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression); } - + /** - * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression - * + * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression + * * @return Doctrine\ORM\Query\AST\WhenExpression */ public function WhenClause() @@ -1807,13 +1835,13 @@ class Parser $this->match(Lexer::T_WHEN); $conditionalExpression = $this->ConditionalExpression(); $this->match(Lexer::T_THEN); - + return new AST\WhenClause($conditionalExpression, $this->ScalarExpression()); } - + /** - * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression - * + * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression + * * @return Doctrine\ORM\Query\AST\SimpleWhenExpression */ public function SimpleWhenClause() @@ -1821,109 +1849,105 @@ class Parser $this->match(Lexer::T_WHEN); $conditionalExpression = $this->ScalarExpression(); $this->match(Lexer::T_THEN); - + return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression()); } /** - * SelectExpression ::= - * IdentificationVariable | StateFieldPathExpression | - * (AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] ["HIDDEN"] AliasResultVariable] + * SelectExpression ::= ( + * IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | + * PartialObjectExpression | "(" Subselect ")" | CaseExpression + * ) [["AS"] ["HIDDEN"] AliasResultVariable] * * @return Doctrine\ORM\Query\AST\SelectExpression */ public function SelectExpression() { - $expression = null; + $expression = null; $identVariable = null; - $hiddenAliasResultVariable = false; - $fieldAliasIdentificationVariable = null; - $peek = $this->_lexer->glimpse(); + $peek = $this->_lexer->glimpse(); - $supportsAlias = true; - - if ($peek['value'] != '(' && $this->_lexer->lookahead['type'] === Lexer::T_IDENTIFIER) { - if ($peek['value'] == '.') { - // ScalarExpression - $expression = $this->ScalarExpression(); - } else { - $supportsAlias = false; - $expression = $identVariable = $this->IdentificationVariable(); - } - } else if ($this->_lexer->lookahead['value'] == '(') { - if ($peek['type'] == Lexer::T_SELECT) { - // Subselect - $this->match(Lexer::T_OPEN_PARENTHESIS); - $expression = $this->Subselect(); - $this->match(Lexer::T_CLOSE_PARENTHESIS); - } else { - // Shortcut: ScalarExpression => SimpleArithmeticExpression - $expression = $this->SimpleArithmeticExpression(); - } + if ($this->_lexer->lookahead['type'] === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT) { + // ScalarExpression (u.name) + $expression = $this->ScalarExpression(); + } else if ($this->_lexer->lookahead['type'] === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS) { + // IdentificationVariable (u) + $expression = $identVariable = $this->IdentificationVariable(); + } else if (in_array($this->_lexer->lookahead['type'], array(Lexer::T_CASE, Lexer::T_COALESCE, Lexer::T_NULLIF))) { + // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...)) + $expression = $this->CaseExpression(); } else if ($this->_isFunction()) { + // DQL Function (SUM(u.value) or SUM(u.value) + 1) $this->_lexer->peek(); // "(" - + $lookaheadType = $this->_lexer->lookahead['type']; $beyond = $this->_peekBeyondClosingParenthesis(); - + if ($this->_isMathOperator($beyond)) { + // SUM(u.id) + COUNT(u.id) $expression = $this->ScalarExpression(); } else if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { + // COUNT(u.id) $expression = $this->AggregateExpression(); - } else if (in_array($lookaheadType, array(Lexer::T_COALESCE, Lexer::T_NULLIF))) { - $expression = $this->CaseExpression(); } else { - // Shortcut: ScalarExpression => Function + // SUM(u.id) $expression = $this->FunctionDeclaration(); } - } else if ($this->_lexer->lookahead['type'] == Lexer::T_PARTIAL) { - $supportsAlias = false; + } else if ($this->_lexer->lookahead['type'] === Lexer::T_PARTIAL) { + // PartialObjectExpression (PARTIAL u.{id, name}) $expression = $this->PartialObjectExpression(); $identVariable = $expression->identificationVariable; - } else if ($this->_lexer->lookahead['type'] == Lexer::T_INTEGER || - $this->_lexer->lookahead['type'] == Lexer::T_FLOAT || - $this->_lexer->lookahead['type'] == Lexer::T_STRING) { + } else if ($this->_lexer->lookahead['type'] === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT) { + // Subselect + $this->match(Lexer::T_OPEN_PARENTHESIS); + $expression = $this->Subselect(); + $this->match(Lexer::T_CLOSE_PARENTHESIS); + } else if (in_array($this->_lexer->lookahead['type'], array(Lexer::T_OPEN_PARENTHESIS, Lexer::T_INTEGER, Lexer::T_FLOAT, Lexer::T_STRING))) { // Shortcut: ScalarExpression => SimpleArithmeticExpression $expression = $this->SimpleArithmeticExpression(); - } else if ($this->_lexer->lookahead['type'] == Lexer::T_CASE) { - $expression = $this->CaseExpression(); } else { $this->syntaxError( - 'IdentificationVariable | StateFieldPathExpression | AggregateExpression | "(" Subselect ")" | ScalarExpression', + 'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression', $this->_lexer->lookahead ); } - if ($supportsAlias) { - 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; - } + // [["AS"] ["HIDDEN"] AliasResultVariable] - if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) { - $token = $this->_lexer->lookahead; - $fieldAliasIdentificationVariable = $this->AliasResultVariable(); - - // Include AliasResultVariable in query components. - $this->_queryComponents[$fieldAliasIdentificationVariable] = array( - 'resultVariable' => $expression, - 'nestingLevel' => $this->_nestingLevel, - 'token' => $token, - ); - } + if ($this->_lexer->isNextToken(Lexer::T_AS)) { + $this->match(Lexer::T_AS); } - $expr = new AST\SelectExpression($expression, $fieldAliasIdentificationVariable, $hiddenAliasResultVariable); - - if ( ! $supportsAlias) { + $hiddenAliasResultVariable = false; + + if ($this->_lexer->isNextToken(Lexer::T_HIDDEN)) { + $this->match(Lexer::T_HIDDEN); + + $hiddenAliasResultVariable = true; + } + + $aliasResultVariable = null; + + if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) { + $token = $this->_lexer->lookahead; + $aliasResultVariable = $this->AliasResultVariable(); + + // Include AliasResultVariable in query components. + $this->_queryComponents[$aliasResultVariable] = array( + 'resultVariable' => $expression, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $token, + ); + } + + // AST + + $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable); + + if ($identVariable) { $this->_identVariableExpressions[$identVariable] = $expr; } - + return $expr; } @@ -1940,11 +1964,9 @@ class Parser if ($peek['value'] != '(' && $this->_lexer->lookahead['type'] === Lexer::T_IDENTIFIER) { // SingleValuedPathExpression | IdentificationVariable - if ($peek['value'] == '.') { - $expression = $this->StateFieldPathExpression(); - } else { - $expression = $this->IdentificationVariable(); - } + $expression = ($peek['value'] == '.') + ? $this->StateFieldPathExpression() + : $this->IdentificationVariable(); return new AST\SimpleSelectExpression($expression); } else if ($this->_lexer->lookahead['value'] == '(') { @@ -1964,8 +1986,7 @@ class Parser $this->_lexer->peek(); $expression = $this->ScalarExpression(); - - $expr = new AST\SimpleSelectExpression($expression); + $expr = new AST\SimpleSelectExpression($expression); if ($this->_lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); @@ -1999,6 +2020,7 @@ class Parser while ($this->_lexer->isNextToken(Lexer::T_OR)) { $this->match(Lexer::T_OR); + $conditionalTerms[] = $this->ConditionalTerm(); } @@ -2023,6 +2045,7 @@ class Parser while ($this->_lexer->isNextToken(Lexer::T_AND)) { $this->match(Lexer::T_AND); + $conditionalFactors[] = $this->ConditionalFactor(); } @@ -2046,9 +2069,10 @@ class Parser if ($this->_lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::T_NOT); + $not = true; } - + $conditionalPrimary = $this->ConditionalPrimary(); // Phase 1 AST optimization: Prevent AST\ConditionalFactor @@ -2203,13 +2227,13 @@ class Parser */ public function CollectionMemberExpression() { - $not = false; - + $not = false; $entityExpr = $this->EntityExpression(); if ($this->_lexer->isNextToken(Lexer::T_NOT)) { - $not = true; $this->match(Lexer::T_NOT); + + $not = true; } $this->match(Lexer::T_MEMBER); @@ -2374,7 +2398,7 @@ class Parser $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS); $sign = $isPlus; } - + $primary = $this->ArithmeticPrimary(); // Phase 1 AST optimization: Prevent AST\ArithmeticFactor @@ -2407,7 +2431,7 @@ class Parser case Lexer::T_NULLIF: case Lexer::T_CASE: return $this->CaseExpression(); - + case Lexer::T_IDENTIFIER: $peek = $this->_lexer->glimpse(); @@ -2418,11 +2442,11 @@ class Parser if ($peek['value'] == '.') { return $this->SingleValuedPathExpression(); } - + if (isset($this->_queryComponents[$this->_lexer->lookahead['value']]['resultVariable'])) { return $this->ResultVariable(); } - + return $this->StateFieldPathExpression(); case Lexer::T_INPUT_PARAMETER: @@ -2703,47 +2727,47 @@ class Parser $this->match(Lexer::T_INSTANCE); $this->match(Lexer::T_OF); - + $exprValues = array(); - + if ($this->_lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) { $this->match(Lexer::T_OPEN_PARENTHESIS); - + $exprValues[] = $this->InstanceOfParameter(); while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); - + $exprValues[] = $this->InstanceOfParameter(); } - + $this->match(Lexer::T_CLOSE_PARENTHESIS); - + $instanceOfExpression->value = $exprValues; - + return $instanceOfExpression; } $exprValues[] = $this->InstanceOfParameter(); $instanceOfExpression->value = $exprValues; - + return $instanceOfExpression; } - + /** * InstanceOfParameter ::= AbstractSchemaName | InputParameter - * + * * @return mixed */ public function InstanceOfParameter() { if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { $this->match(Lexer::T_INPUT_PARAMETER); - + return new AST\InputParameter($this->_lexer->token['value']); } - + return $this->AliasIdentificationVariable(); } @@ -2829,8 +2853,10 @@ class Parser $this->match(Lexer::T_EXISTS); $this->match(Lexer::T_OPEN_PARENTHESIS); + $existsExpression = new AST\ExistsExpression($this->Subselect()); $existsExpression->not = $not; + $this->match(Lexer::T_CLOSE_PARENTHESIS); return $existsExpression; @@ -2894,26 +2920,45 @@ class Parser $funcName = strtolower($token['value']); // Check for built-in functions first! - if (isset(self::$_STRING_FUNCTIONS[$funcName])) { - return $this->FunctionsReturningStrings(); - } else if (isset(self::$_NUMERIC_FUNCTIONS[$funcName])) { - return $this->FunctionsReturningNumerics(); - } else if (isset(self::$_DATETIME_FUNCTIONS[$funcName])) { - return $this->FunctionsReturningDatetime(); + switch (true) { + case (isset(self::$_STRING_FUNCTIONS[$funcName])): + return $this->FunctionsReturningStrings(); + + case (isset(self::$_NUMERIC_FUNCTIONS[$funcName])): + return $this->FunctionsReturningNumerics(); + + case (isset(self::$_DATETIME_FUNCTIONS[$funcName])): + return $this->FunctionsReturningDatetime(); + + default: + return $this->CustomFunctionDeclaration(); } + } + + /** + * Helper function for FunctionDeclaration grammar rule + */ + private function CustomFunctionDeclaration() + { + $token = $this->_lexer->lookahead; + $funcName = strtolower($token['value']); // Check for custom functions afterwards $config = $this->_em->getConfiguration(); - if ($config->getCustomStringFunction($funcName) !== null) { - return $this->CustomFunctionsReturningStrings(); - } else if ($config->getCustomNumericFunction($funcName) !== null) { - return $this->CustomFunctionsReturningNumerics(); - } else if ($config->getCustomDatetimeFunction($funcName) !== null) { - return $this->CustomFunctionsReturningDatetime(); - } + switch (true) { + case ($config->getCustomStringFunction($funcName) !== null): + return $this->CustomFunctionsReturningStrings(); - $this->syntaxError('known function', $token); + case ($config->getCustomNumericFunction($funcName) !== null): + return $this->CustomFunctionsReturningNumerics(); + + case ($config->getCustomDatetimeFunction($funcName) !== null): + return $this->CustomFunctionsReturningDatetime(); + + default: + $this->syntaxError('known function', $token); + } } /** @@ -2928,7 +2973,8 @@ class Parser public function FunctionsReturningNumerics() { $funcNameLower = strtolower($this->_lexer->lookahead['value']); - $funcClass = self::$_NUMERIC_FUNCTIONS[$funcNameLower]; + $funcClass = self::$_NUMERIC_FUNCTIONS[$funcNameLower]; + $function = new $funcClass($funcNameLower); $function->parse($this); @@ -2937,9 +2983,10 @@ class Parser public function CustomFunctionsReturningNumerics() { - $funcName = strtolower($this->_lexer->lookahead['value']); // getCustomNumericFunction is case-insensitive + $funcName = strtolower($this->_lexer->lookahead['value']); $funcClass = $this->_em->getConfiguration()->getCustomNumericFunction($funcName); + $function = new $funcClass($funcName); $function->parse($this); @@ -2952,7 +2999,8 @@ class Parser public function FunctionsReturningDatetime() { $funcNameLower = strtolower($this->_lexer->lookahead['value']); - $funcClass = self::$_DATETIME_FUNCTIONS[$funcNameLower]; + $funcClass = self::$_DATETIME_FUNCTIONS[$funcNameLower]; + $function = new $funcClass($funcNameLower); $function->parse($this); @@ -2961,9 +3009,10 @@ class Parser public function CustomFunctionsReturningDatetime() { - $funcName = $this->_lexer->lookahead['value']; // getCustomDatetimeFunction is case-insensitive + $funcName = $this->_lexer->lookahead['value']; $funcClass = $this->_em->getConfiguration()->getCustomDatetimeFunction($funcName); + $function = new $funcClass($funcName); $function->parse($this); @@ -2981,7 +3030,8 @@ class Parser public function FunctionsReturningStrings() { $funcNameLower = strtolower($this->_lexer->lookahead['value']); - $funcClass = self::$_STRING_FUNCTIONS[$funcNameLower]; + $funcClass = self::$_STRING_FUNCTIONS[$funcNameLower]; + $function = new $funcClass($funcNameLower); $function->parse($this); @@ -2990,9 +3040,10 @@ class Parser public function CustomFunctionsReturningStrings() { - $funcName = $this->_lexer->lookahead['value']; // getCustomStringFunction is case-insensitive + $funcName = $this->_lexer->lookahead['value']; $funcClass = $this->_em->getConfiguration()->getCustomStringFunction($funcName); + $function = new $funcClass($funcName); $function->parse($this); diff --git a/lib/Doctrine/ORM/Query/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index 5ac99ca82..0d9fed0b9 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMapping.php +++ b/lib/Doctrine/ORM/Query/ResultSetMapping.php @@ -26,7 +26,7 @@ namespace Doctrine\ORM\Query; * The properties of this class are only public for fast internal READ access and to (drastically) * reduce the size of serialized instances for more effective caching due to better (un-)serialization * performance. - * + * * Users should use the public methods. * * @author Roman Borschel @@ -36,87 +36,79 @@ namespace Doctrine\ORM\Query; class ResultSetMapping { /** - * Whether the result is mixed (contains scalar values together with field values). - * * @ignore - * @var boolean + * @var boolean Whether the result is mixed (contains scalar values together with field values). */ public $isMixed = false; + /** - * Maps alias names to class names. - * * @ignore - * @var array + * @var array Maps alias names to class names. */ public $aliasMap = array(); + /** - * Maps alias names to related association field names. - * * @ignore - * @var array + * @var array Maps alias names to related association field names. */ public $relationMap = array(); + /** - * Maps alias names to parent alias names. - * * @ignore - * @var array + * @var array Maps alias names to parent alias names. */ public $parentAliasMap = array(); + /** - * Maps column names in the result set to field names for each class. - * * @ignore - * @var array + * @var array Maps column names in the result set to field names for each class. */ public $fieldMappings = array(); + /** - * Maps column names in the result set to the alias/field name to use in the mapped result. - * * @ignore - * @var array + * @var array Maps column names in the result set to the alias/field name to use in the mapped result. */ public $scalarMappings = array(); + /** - * Maps column names of meta columns (foreign keys, discriminator columns, ...) to field names. - * * @ignore - * @var array + * @var array Maps entities in the result set to the alias name to use in the mapped result. + */ + public $entityMappings = array(); + + /** + * @ignore + * @var array Maps column names of meta columns (foreign keys, discriminator columns, ...) to field names. */ public $metaMappings = array(); + /** - * Maps column names in the result set to the alias they belong to. - * * @ignore - * @var array + * @var array Maps column names in the result set to the alias they belong to. */ public $columnOwnerMap = array(); + /** - * List of columns in the result set that are used as discriminator columns. - * * @ignore - * @var array + * @var array List of columns in the result set that are used as discriminator columns. */ public $discriminatorColumns = array(); + /** - * Maps alias names to field names that should be used for indexing. - * * @ignore - * @var array + * @var array Maps alias names to field names that should be used for indexing. */ public $indexByMap = array(); + /** - * Map from column names to class names that declare the field the column is mapped to. - * * @ignore - * @var array + * @var array Map from column names to class names that declare the field the column is mapped to. */ public $declaringClasses = array(); - + /** - * This is necessary to hydrate derivate foreign keys correctly. - * - * @var array + * @var array This is necessary to hydrate derivate foreign keys correctly. */ public $isIdentifierColumn = array(); @@ -126,11 +118,15 @@ class ResultSetMapping * @param string $class The class name of the entity. * @param string $alias The alias for the class. The alias must be unique among all entity * results or joined entity results within this ResultSetMapping. + * @param string $resultAlias The result alias with which the entity result should be + * placed in the result structure. + * * @todo Rename: addRootEntity */ - public function addEntityResult($class, $alias) + public function addEntityResult($class, $alias, $resultAlias = null) { $this->aliasMap[$alias] = $class; + $this->entityMappings[$alias] = $resultAlias; } /** @@ -141,6 +137,7 @@ class ResultSetMapping * @param string $alias The alias of the entity result or joined entity result the discriminator * column should be used for. * @param string $discrColumn The name of the discriminator column in the SQL result set. + * * @todo Rename: addDiscriminatorColumn */ public function setDiscriminatorColumn($alias, $discrColumn) @@ -158,20 +155,27 @@ class ResultSetMapping public function addIndexBy($alias, $fieldName) { $found = false; + foreach ($this->fieldMappings AS $columnName => $columnFieldName) { - if ($columnFieldName === $fieldName && $this->columnOwnerMap[$columnName] == $alias) { - $this->addIndexByColumn($alias, $columnName); - $found = true; - break; - } + if ( ! ($columnFieldName === $fieldName && $this->columnOwnerMap[$columnName] === $alias)) continue; + + $this->addIndexByColumn($alias, $columnName); + $found = true; + + break; } /* TODO: check if this exception can be put back, for now it's gone because of assumptions made by some ORM internals - if (!$found) { - throw new \LogicException("Cannot add index by for dql alias " . $alias . " and field " . - $fieldName . " without calling addFieldResult() for them before."); + if ( ! $found) { + $message = sprintf( + 'Cannot add index by for DQL alias %s and field %s without calling addFieldResult() for them before.', + $alias, + $fieldName + ); + + throw new \LogicException($message); } - */ + */ } /** @@ -244,6 +248,7 @@ class ResultSetMapping $this->columnOwnerMap[$columnName] = $alias; // field name => class name of declaring class $this->declaringClasses[$columnName] = $declaringClass ?: $this->aliasMap[$alias]; + if ( ! $this->isMixed && $this->scalarMappings) { $this->isMixed = true; } @@ -260,11 +265,11 @@ class ResultSetMapping */ public function addJoinedEntityResult($class, $alias, $parentAlias, $relation) { - $this->aliasMap[$alias] = $class; + $this->aliasMap[$alias] = $class; $this->parentAliasMap[$alias] = $parentAlias; - $this->relationMap[$alias] = $relation; + $this->relationMap[$alias] = $relation; } - + /** * Adds a scalar result mapping. * @@ -275,6 +280,7 @@ class ResultSetMapping public function addScalarResult($columnName, $alias) { $this->scalarMappings[$columnName] = $alias; + if ( ! $this->isMixed && $this->fieldMappings) { $this->isMixed = true; } @@ -282,7 +288,7 @@ class ResultSetMapping /** * Checks whether a column with a given name is mapped as a scalar result. - * + * * @param string $columName The name of the column in the SQL result set. * @return boolean * @todo Rename: isScalar @@ -421,10 +427,10 @@ class ResultSetMapping { return $this->isMixed; } - + /** * Adds a meta column (foreign key or discriminator column) to the result set. - * + * * @param string $alias * @param string $columnName * @param string $fieldName @@ -434,6 +440,7 @@ class ResultSetMapping { $this->metaMappings[$columnName] = $fieldName; $this->columnOwnerMap[$columnName] = $alias; + if ($isIdentifierColumn) { $this->isIdentifierColumn[$alias][$columnName] = true; } diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 03933d3d9..ba5d29714 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -28,9 +28,10 @@ use Doctrine\DBAL\LockMode, * The SqlWalker is a TreeWalker that walks over a DQL AST and constructs * the corresponding SQL. * + * @author Guilherme Blanco * @author Roman Borschel * @author Benjamin Eberlei - * @since 2.0 + * @since 2.0 * @todo Rename: SQLWalker */ class SqlWalker implements TreeWalker @@ -257,13 +258,13 @@ class SqlWalker implements TreeWalker // If this is a joined association we must use left joins to preserve the correct result. $sql .= isset($this->_queryComponents[$dqlAlias]['relation']) ? ' LEFT ' : ' INNER '; $sql .= 'JOIN ' . $parentClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON '; - + $sqlParts = array(); foreach ($class->getQuotedIdentifierColumnNames($this->_platform) as $columnName) { $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; } - + $sql .= implode(' AND ', $sqlParts); } @@ -271,7 +272,7 @@ class SqlWalker implements TreeWalker if ($this->_query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) { return $sql; } - + // LEFT JOIN child class tables foreach ($class->subClasses as $subClassName) { $subClass = $this->_em->getClassMetadata($subClassName); @@ -294,18 +295,19 @@ class SqlWalker implements TreeWalker private function _generateOrderedCollectionOrderByItems() { $sqlParts = array(); - - foreach ($this->_selectedClasses AS $dqlAlias => $class) { - $qComp = $this->_queryComponents[$dqlAlias]; + + foreach ($this->_selectedClasses AS $selectedClass) { + $dqlAlias = $selectedClass['dqlAlias']; + $qComp = $this->_queryComponents[$dqlAlias]; if ( ! isset($qComp['relation']['orderBy'])) continue; - + foreach ($qComp['relation']['orderBy'] AS $fieldName => $orientation) { $columnName = $qComp['metadata']->getQuotedColumnName($fieldName, $this->_platform); $tableName = ($qComp['metadata']->isInheritanceTypeJoined()) - ? $this->_em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName) + ? $this->_em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name)->getOwningTable($fieldName) : $qComp['metadata']->getTableName(); - + $sqlParts[] = $this->getSQLTableAlias($tableName, $dqlAlias) . '.' . $columnName . ' ' . $orientation; } } @@ -327,7 +329,7 @@ class SqlWalker implements TreeWalker $class = $this->_queryComponents[$dqlAlias]['metadata']; if ( ! $class->isInheritanceTypeSingleTable()) continue; - + $conn = $this->_em->getConnection(); $values = array(); @@ -344,7 +346,7 @@ class SqlWalker implements TreeWalker } $sql = implode(' AND ', $sqlParts); - + return (count($sqlParts) > 1) ? '(' . $sql . ')' : $sql; } @@ -376,19 +378,19 @@ class SqlWalker implements TreeWalker case LockMode::PESSIMISTIC_READ: $sql .= ' ' . $this->_platform->getReadLockSQL(); break; - + case LockMode::PESSIMISTIC_WRITE: $sql .= ' ' . $this->_platform->getWriteLockSQL(); break; - + case LockMode::PESSIMISTIC_OPTIMISTIC: - foreach ($this->_selectedClasses AS $class) { + foreach ($this->_selectedClasses AS $selectedClass) { if ( ! $class->isVersioned) { - throw \Doctrine\ORM\OptimisticLockException::lockFailed($class->name); + throw \Doctrine\ORM\OptimisticLockException::lockFailed($selectedClass['class']->name); } } break; - + default: throw \Doctrine\ORM\Query\QueryException::invalidLockMode(); } @@ -521,13 +523,18 @@ class SqlWalker implements TreeWalker $this->_query->getHydrationMode() != Query::HYDRATE_OBJECT && $this->_query->getHint(Query::HINT_INCLUDE_META_COLUMNS); - foreach ($this->_selectedClasses as $dqlAlias => $class) { + foreach ($this->_selectedClasses as $selectedClass) { + $class = $selectedClass['class']; + $dqlAlias = $selectedClass['dqlAlias']; + $resultAlias = $selectedClass['resultAlias']; + // Register as entity or joined entity result if ($this->_queryComponents[$dqlAlias]['relation'] === null) { - $this->_rsm->addEntityResult($class->name, $dqlAlias); + $this->_rsm->addEntityResult($class->name, $dqlAlias, $resultAlias); } else { $this->_rsm->addJoinedEntityResult( - $class->name, $dqlAlias, + $class->name, + $dqlAlias, $this->_queryComponents[$dqlAlias]['parent'], $this->_queryComponents[$dqlAlias]['relation']['fieldName'] ); @@ -545,10 +552,10 @@ class SqlWalker implements TreeWalker $this->_rsm->setDiscriminatorColumn($dqlAlias, $columnAlias); $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $discrColumn['fieldName']); } - + // Add foreign key columns to SQL, if necessary if ( ! $addMetaColumns) continue; - + // Add foreign key columns of class and also parent classes foreach ($class->associationMappings as $assoc) { if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) continue; @@ -564,7 +571,7 @@ class SqlWalker implements TreeWalker $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $srcColumn, (isset($assoc['id']) && $assoc['id'] === true)); } } - + // Add foreign key columns of subclasses foreach ($class->subClasses as $subClassName) { $subClass = $this->_em->getClassMetadata($subClassName); @@ -573,12 +580,12 @@ class SqlWalker implements TreeWalker foreach ($subClass->associationMappings as $assoc) { // Skip if association is inherited if (isset($assoc['inherited'])) continue; - + if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) continue; - + foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { $columnAlias = $this->getSQLColumnAlias($srcColumn); - + $sqlSelectExpressions[] = $sqlTableAlias . '.' . $srcColumn . ' AS ' . $columnAlias; $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $srcColumn); @@ -661,11 +668,11 @@ class SqlWalker implements TreeWalker public function walkOrderByClause($orderByClause) { $orderByItems = array_map(array($this, 'walkOrderByItem'), $orderByClause->orderByItems); - + if (($collectionOrderByItems = $this->_generateOrderedCollectionOrderByItems()) !== '') { $orderByItems = array_merge($orderByItems, (array) $collectionOrderByItems); } - + return ' ORDER BY ' . implode(', ', $orderByItems); } @@ -709,7 +716,7 @@ class SqlWalker implements TreeWalker $sql = ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) ? ' LEFT JOIN ' : ' INNER JOIN '; - + if ($joinVarDecl->indexBy) { // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. $this->_rsm->addIndexBy( @@ -995,7 +1002,7 @@ class SqlWalker implements TreeWalker $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); $columnName = $class->getQuotedColumnName($fieldName, $this->_platform); $columnAlias = $this->getSQLColumnAlias($columnName); - + $sql .= $sqlTableAlias . '.' . $columnName . ' AS ' . $columnAlias; if ( ! $hidden) { @@ -1003,7 +1010,7 @@ class SqlWalker implements TreeWalker $this->_scalarFields[$dqlAlias][$fieldName] = $columnAlias; } break; - + case ($expr instanceof AST\AggregateExpression): case ($expr instanceof AST\Functions\FunctionNode): case ($expr instanceof AST\SimpleArithmeticExpression): @@ -1017,29 +1024,29 @@ class SqlWalker implements TreeWalker case ($expr instanceof AST\SimpleCaseExpression): $columnAlias = $this->getSQLColumnAlias('sclr'); $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->_scalarResultCounter++; - + $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; - + $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; if ( ! $hidden) { $this->_rsm->addScalarResult($columnAlias, $resultAlias); } break; - + case ($expr instanceof AST\Subselect): $columnAlias = $this->getSQLColumnAlias('sclr'); $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->_scalarResultCounter++; - - $sql .= '(' . $this->walkSubselect($expr) . ') AS '.$columnAlias; - + + $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; + $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; if ( ! $hidden) { $this->_rsm->addScalarResult($columnAlias, $resultAlias); } break; - + default: // IdentificationVariable or PartialObjectExpression if ($expr instanceof AST\PartialObjectExpression) { @@ -1050,18 +1057,23 @@ class SqlWalker implements TreeWalker $partialFieldSet = array(); } - $queryComp = $this->_queryComponents[$dqlAlias]; - $class = $queryComp['metadata']; + $queryComp = $this->_queryComponents[$dqlAlias]; + $class = $queryComp['metadata']; + $resultAlias = $selectExpression->fieldIdentificationVariable ?: null; if ( ! isset($this->_selectedClasses[$dqlAlias])) { - $this->_selectedClasses[$dqlAlias] = $class; + $this->_selectedClasses[$dqlAlias] = array( + 'class' => $class, + 'dqlAlias' => $dqlAlias, + 'resultAlias' => $resultAlias + ); } - $beginning = true; + $sqlParts = array(); // Select all fields from the queried class foreach ($class->fieldMappings as $fieldName => $mapping) { - if ($partialFieldSet && !in_array($fieldName, $partialFieldSet)) { + if ($partialFieldSet && ! in_array($fieldName, $partialFieldSet)) { continue; } @@ -1069,12 +1081,11 @@ class SqlWalker implements TreeWalker ? $this->_em->getClassMetadata($mapping['inherited'])->getTableName() : $class->getTableName(); - if ($beginning) $beginning = false; else $sql .= ', '; + $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); + $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); + $quotedColumnName = $class->getQuotedColumnName($fieldName, $this->_platform); - $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); - $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); - $sql .= $sqlTableAlias . '.' . $class->getQuotedColumnName($fieldName, $this->_platform) - . ' AS ' . $columnAlias; + $sqlParts[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS '. $columnAlias; $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); } @@ -1085,7 +1096,7 @@ class SqlWalker implements TreeWalker // since it requires outer joining subtables. if ($class->isInheritanceTypeSingleTable() || ! $this->_query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) { foreach ($class->subClasses as $subClassName) { - $subClass = $this->_em->getClassMetadata($subClassName); + $subClass = $this->_em->getClassMetadata($subClassName); $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); foreach ($subClass->fieldMappings as $fieldName => $mapping) { @@ -1093,16 +1104,17 @@ class SqlWalker implements TreeWalker continue; } - if ($beginning) $beginning = false; else $sql .= ', '; + $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); + $quotedColumnName = $subClass->getQuotedColumnName($fieldName, $this->_platform); - $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); - $sql .= $sqlTableAlias . '.' . $subClass->getQuotedColumnName($fieldName, $this->_platform) - . ' AS ' . $columnAlias; + $sqlParts[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); } } } + + $sql .= implode(', ', $sqlParts); } return $sql; @@ -1116,8 +1128,7 @@ class SqlWalker implements TreeWalker */ public function walkQuantifiedExpression($qExpr) { - return ' ' . strtoupper($qExpr->type) - . '(' . $this->walkSubselect($qExpr->subselect) . ')'; + return ' ' . strtoupper($qExpr->type) . '(' . $this->walkSubselect($qExpr->subselect) . ')'; } /** @@ -1128,20 +1139,21 @@ class SqlWalker implements TreeWalker */ public function walkSubselect($subselect) { - $useAliasesBefore = $this->_useSqlTableAliases; + $useAliasesBefore = $this->_useSqlTableAliases; $rootAliasesBefore = $this->_rootAliases; $this->_rootAliases = array(); // reset the rootAliases for the subselect $this->_useSqlTableAliases = true; - $sql = $this->walkSimpleSelectClause($subselect->simpleSelectClause); + $sql = $this->walkSimpleSelectClause($subselect->simpleSelectClause); $sql .= $this->walkSubselectFromClause($subselect->subselectFromClause); $sql .= $this->walkWhereClause($subselect->whereClause); + $sql .= $subselect->groupByClause ? $this->walkGroupByClause($subselect->groupByClause) : ''; $sql .= $subselect->havingClause ? $this->walkHavingClause($subselect->havingClause) : ''; $sql .= $subselect->orderByClause ? $this->walkOrderByClause($subselect->orderByClause) : ''; - $this->_rootAliases = $rootAliasesBefore; // put the main aliases back + $this->_rootAliases = $rootAliasesBefore; // put the main aliases back $this->_useSqlTableAliases = $useAliasesBefore; return $sql; @@ -1162,7 +1174,7 @@ class SqlWalker implements TreeWalker $sql = ''; $rangeDecl = $subselectIdVarDecl->rangeVariableDeclaration; - $dqlAlias = $rangeDecl->aliasIdentificationVariable; + $dqlAlias = $rangeDecl->aliasIdentificationVariable; $class = $this->_em->getClassMetadata($rangeDecl->abstractSchemaName); $sql .= $class->getQuotedTableName($this->_platform) . ' ' diff --git a/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php index 616f19146..dfd40b9ff 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php @@ -9,13 +9,34 @@ require_once __DIR__ . '/../../TestInit.php'; class ArrayHydratorTest extends HydrationTestCase { + public function provideDataForUserEntityResult() + { + return array( + array(0), + array('user'), + ); + } + + public function provideDataForMultipleRootEntityResult() + { + return array( + array(0, 0), + array('user', 0), + array(0, 'article'), + array('user', 'article'), + ); + } + /** - * Select u.id, u.name from Doctrine\Tests\Models\CMS\CmsUser u + * SELECT PARTIAL u.{id, name} + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * + * @dataProvider provideDataForUserEntityResult */ - public function testSimpleEntityQuery() + public function testSimpleEntityQuery($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); @@ -24,35 +45,39 @@ class ArrayHydratorTest extends HydrationTestCase array( 'u__id' => '1', 'u__name' => 'romanb' - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage' - ) - ); + ) + ); - - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); + $this->assertEquals(1, $result[0]['id']); $this->assertEquals('romanb', $result[0]['name']); + $this->assertEquals(2, $result[1]['id']); $this->assertEquals('jwage', $result[1]['name']); } /** - * + * SELECT PARTIAL u.{id, name}, PARTIAL a.{id, topic} + * FROM Doctrine\Tests\Models\CMS\CmsUser u, Doctrine\Tests\Models\CMS\CmsArticle a + * + * @dataProvider provideDataForMultipleRootEntityResult */ - public function testSimpleMultipleRootEntityQuery() + public function testSimpleMultipleRootEntityQuery($userEntityKey, $articleEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsArticle', 'a'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsArticle', 'a', $articleEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); $rsm->addFieldResult('a', 'a__id', 'id'); @@ -65,47 +90,51 @@ class ArrayHydratorTest extends HydrationTestCase 'u__name' => 'romanb', 'a__id' => '1', 'a__topic' => 'Cool things.' - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'a__id' => '2', 'a__topic' => 'Cool things II.' - ) - ); + ) + ); - - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(4, count($result)); $this->assertEquals(1, $result[0]['id']); $this->assertEquals('romanb', $result[0]['name']); + $this->assertEquals(1, $result[1]['id']); $this->assertEquals('Cool things.', $result[1]['topic']); + $this->assertEquals(2, $result[2]['id']); $this->assertEquals('jwage', $result[2]['name']); + $this->assertEquals(2, $result[3]['id']); $this->assertEquals('Cool things II.', $result[3]['topic']); } /** - * select u.id, u.status, p.phonenumber, upper(u.name) nameUpper from User u - * join u.phonenumbers p - * = - * select u.id, u.status, p.phonenumber, upper(u.name) as u__0 from USERS u - * INNER JOIN PHONENUMBERS p ON u.id = p.user_id + * SELECT PARTIAL u.{id, status}, PARTIAL p.{phonenumber}, UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * JOIN u.phonenumbers p + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryFetchJoin() + public function testMixedQueryFetchJoin($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', 'p', - 'u', 'phonenumbers'); + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' + ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'nameUpper'); @@ -119,54 +148,56 @@ class ArrayHydratorTest extends HydrationTestCase 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '42', - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', 'p__phonenumber' => '91' - ) - ); + ) + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); $this->assertTrue(is_array($result[0])); $this->assertTrue(is_array($result[1])); // first user => 2 phonenumbers - $this->assertEquals(2, count($result[0][0]['phonenumbers'])); + $this->assertEquals(2, count($result[0][$userEntityKey]['phonenumbers'])); $this->assertEquals('ROMANB', $result[0]['nameUpper']); + // second user => 1 phonenumber - $this->assertEquals(1, count($result[1][0]['phonenumbers'])); + $this->assertEquals(1, count($result[1][$userEntityKey]['phonenumbers'])); $this->assertEquals('JWAGE', $result[1]['nameUpper']); - $this->assertEquals(42, $result[0][0]['phonenumbers'][0]['phonenumber']); - $this->assertEquals(43, $result[0][0]['phonenumbers'][1]['phonenumber']); - $this->assertEquals(91, $result[1][0]['phonenumbers'][0]['phonenumber']); + $this->assertEquals(42, $result[0][$userEntityKey]['phonenumbers'][0]['phonenumber']); + $this->assertEquals(43, $result[0][$userEntityKey]['phonenumbers'][1]['phonenumber']); + $this->assertEquals(91, $result[1][$userEntityKey]['phonenumbers'][0]['phonenumber']); } /** - * select u.id, u.status, count(p.phonenumber) numPhones from User u - * join u.phonenumbers p group by u.status, u.id - * = - * select u.id, u.status, count(p.phonenumber) as p__0 from USERS u - * INNER JOIN PHONENUMBERS p ON u.id = p.user_id group by u.id, u.status + * SELECT PARTIAL u.{id, status}, COUNT(p.phonenumber) AS numPhones + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * JOIN u.phonenumbers p + * GROUP BY u.status, u.id + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryNormalJoin() + public function testMixedQueryNormalJoin($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'numPhones'); @@ -178,45 +209,50 @@ class ArrayHydratorTest extends HydrationTestCase 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => '2', - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => '1', - ) - ); + ) + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); $this->assertTrue(is_array($result)); $this->assertTrue(is_array($result[0])); $this->assertTrue(is_array($result[1])); + // first user => 2 phonenumbers + $this->assertArrayHasKey($userEntityKey, $result[0]); $this->assertEquals(2, $result[0]['numPhones']); + // second user => 1 phonenumber + $this->assertArrayHasKey($userEntityKey, $result[1]); $this->assertEquals(1, $result[1]['numPhones']); } /** - * select u.id, u.status, upper(u.name) nameUpper from User u index by u.id - * join u.phonenumbers p indexby p.phonenumber - * = - * select u.id, u.status, upper(u.name) as p__0 from USERS u - * INNER JOIN PHONENUMBERS p ON u.id = p.user_id + * SELECT PARTIAL u.{id, status}, UPPER(u.name) nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * INDEX BY u.id + * JOIN u.phonenumbers p + * INDEX BY p.phonenumber + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryFetchJoinCustomIndex() + public function testMixedQueryFetchJoinCustomIndex($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', - 'p', - 'u', - 'phonenumbers' + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -233,28 +269,28 @@ class ArrayHydratorTest extends HydrationTestCase 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '42', - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', 'p__phonenumber' => '91' - ) - ); + ) + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); $this->assertTrue(is_array($result[1])); $this->assertTrue(is_array($result[2])); @@ -262,14 +298,17 @@ class ArrayHydratorTest extends HydrationTestCase // test the scalar values $this->assertEquals('ROMANB', $result[1]['nameUpper']); $this->assertEquals('JWAGE', $result[2]['nameUpper']); + // first user => 2 phonenumbers. notice the custom indexing by user id - $this->assertEquals(2, count($result[1][0]['phonenumbers'])); + $this->assertEquals(2, count($result[1][$userEntityKey]['phonenumbers'])); + // second user => 1 phonenumber. notice the custom indexing by user id - $this->assertEquals(1, count($result[2][0]['phonenumbers'])); + $this->assertEquals(1, count($result[2][$userEntityKey]['phonenumbers'])); + // test the custom indexing of the phonenumbers - $this->assertTrue(isset($result[1][0]['phonenumbers']['42'])); - $this->assertTrue(isset($result[1][0]['phonenumbers']['43'])); - $this->assertTrue(isset($result[2][0]['phonenumbers']['91'])); + $this->assertTrue(isset($result[1][$userEntityKey]['phonenumbers']['42'])); + $this->assertTrue(isset($result[1][$userEntityKey]['phonenumbers']['43'])); + $this->assertTrue(isset($result[2][$userEntityKey]['phonenumbers']['91'])); } /** @@ -288,16 +327,16 @@ class ArrayHydratorTest extends HydrationTestCase $rsm = new ResultSetMapping; $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', - 'p', - 'u', - 'phonenumbers' + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' ); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsArticle', - 'a', - 'u', - 'articles' + 'Doctrine\Tests\Models\CMS\CmsArticle', + 'a', + 'u', + 'articles' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -316,15 +355,15 @@ class ArrayHydratorTest extends HydrationTestCase 'p__phonenumber' => '42', 'a__id' => '1', 'a__topic' => 'Getting things done!' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', 'a__id' => '1', 'a__topic' => 'Getting things done!' - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', @@ -332,15 +371,15 @@ class ArrayHydratorTest extends HydrationTestCase 'p__phonenumber' => '42', 'a__id' => '2', 'a__topic' => 'ZendCon' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', 'a__id' => '2', 'a__topic' => 'ZendCon' - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', @@ -348,21 +387,20 @@ class ArrayHydratorTest extends HydrationTestCase 'p__phonenumber' => '91', 'a__id' => '3', 'a__topic' => 'LINQ' - ), - array( + ), + array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', 'p__phonenumber' => '91', 'a__id' => '4', 'a__topic' => 'PHP6' - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); $this->assertTrue(is_array($result)); @@ -408,22 +446,22 @@ class ArrayHydratorTest extends HydrationTestCase $rsm = new ResultSetMapping; $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', - 'p', - 'u', - 'phonenumbers' + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' ); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsArticle', - 'a', - 'u', - 'articles' + 'Doctrine\Tests\Models\CMS\CmsArticle', + 'a', + 'u', + 'articles' ); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsComment', - 'c', - 'a', - 'comments' + 'Doctrine\Tests\Models\CMS\CmsComment', + 'c', + 'a', + 'comments' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -446,8 +484,8 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'Getting things done!', 'c__id' => '1', 'c__topic' => 'First!' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', @@ -456,7 +494,7 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'Getting things done!', 'c__id' => '1', 'c__topic' => 'First!' - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', @@ -466,8 +504,8 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'ZendCon', 'c__id' => null, 'c__topic' => null - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', @@ -476,7 +514,7 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'ZendCon', 'c__id' => null, 'c__topic' => null - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', @@ -486,8 +524,8 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'LINQ', 'c__id' => null, 'c__topic' => null - ), - array( + ), + array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', @@ -496,13 +534,12 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'PHP6', 'c__id' => null, 'c__topic' => null - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); $this->assertTrue(is_array($result)); @@ -565,11 +602,12 @@ class ArrayHydratorTest extends HydrationTestCase $rsm = new ResultSetMapping; $rsm->addEntityResult('Doctrine\Tests\Models\Forum\ForumCategory', 'c'); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\Forum\ForumBoard', - 'b', - 'c', - 'boards' + 'Doctrine\Tests\Models\Forum\ForumBoard', + 'b', + 'c', + 'boards' ); + $rsm->addFieldResult('c', 'c__id', 'id'); $rsm->addFieldResult('c', 'c__position', 'position'); $rsm->addFieldResult('c', 'c__name', 'name'); @@ -585,15 +623,15 @@ class ArrayHydratorTest extends HydrationTestCase 'b__id' => '1', 'b__position' => '0', //'b__category_id' => '1' - ), - array( + ), + array( 'c__id' => '2', 'c__position' => '0', 'c__name' => 'Second', 'b__id' => '2', 'b__position' => '0', //'b__category_id' => '2' - ), + ), array( 'c__id' => '1', 'c__position' => '0', @@ -601,21 +639,20 @@ class ArrayHydratorTest extends HydrationTestCase 'b__id' => '3', 'b__position' => '1', //'b__category_id' => '1' - ), - array( + ), + array( 'c__id' => '1', 'c__position' => '0', 'c__name' => 'First', 'b__id' => '4', 'b__position' => '2', //'b__category_id' => '1' - ) - ); + ) + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); $this->assertTrue(is_array($result)); @@ -626,15 +663,19 @@ class ArrayHydratorTest extends HydrationTestCase $this->assertTrue(isset($result[1]['boards'])); $this->assertEquals(1, count($result[1]['boards'])); } - + /** - * DQL: select partial u.{id,status}, a.id, a.topic, c.id as cid, c.topic as ctopic from CmsUser u left join u.articles a left join a.comments c - * + * SELECT PARTIAL u.{id,status}, a.id, a.topic, c.id as cid, c.topic as ctopic + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * LEFT JOIN u.articles a + * LEFT JOIN a.comments c + * + * @dataProvider provideDataForUserEntityResult */ - /*public function testChainedJoinWithScalars() + public function testChainedJoinWithScalars($entityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $entityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('a__id', 'id'); @@ -652,7 +693,7 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'The First', 'c__id' => '1', 'c__topic' => 'First Comment' - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', @@ -660,47 +701,52 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'The First', 'c__id' => '2', 'c__topic' => 'Second Comment' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'a__id' => '42', 'a__topic' => 'The Answer', 'c__id' => null, 'c__topic' => null - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); + $result = $hydrator->hydrateAll($stmt, $rsm); - $result = $hydrator->hydrateAll($stmt, $rsm); - $this->assertEquals(3, count($result)); - - $this->assertEquals(2, count($result[0][0])); // User array + + $this->assertEquals(2, count($result[0][$entityKey])); // User array $this->assertEquals(1, $result[0]['id']); $this->assertEquals('The First', $result[0]['topic']); $this->assertEquals(1, $result[0]['cid']); $this->assertEquals('First Comment', $result[0]['ctopic']); - - $this->assertEquals(2, count($result[1][0])); // User array, duplicated + + $this->assertEquals(2, count($result[1][$entityKey])); // User array, duplicated $this->assertEquals(1, $result[1]['id']); // duplicated $this->assertEquals('The First', $result[1]['topic']); // duplicated $this->assertEquals(2, $result[1]['cid']); $this->assertEquals('Second Comment', $result[1]['ctopic']); - - $this->assertEquals(2, count($result[2][0])); // User array, duplicated + + $this->assertEquals(2, count($result[2][$entityKey])); // User array, duplicated $this->assertEquals(42, $result[2]['id']); $this->assertEquals('The Answer', $result[2]['topic']); $this->assertNull($result[2]['cid']); $this->assertNull($result[2]['ctopic']); - }*/ + } - public function testResultIteration() + /** + * SELECT PARTIAL u.{id, status}, UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * + * @dataProvider provideDataForUserEntityResult + */ + public function testResultIteration($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); @@ -709,23 +755,22 @@ class ArrayHydratorTest extends HydrationTestCase array( 'u__id' => '1', 'u__name' => 'romanb' - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage' - ) - ); + ) + ); - - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); + $iterator = $hydrator->iterate($stmt, $rsm); + $rowNum = 0; - $iterableResult = $hydrator->iterate($stmt, $rsm); - - $rowNum = 0; - while (($row = $iterableResult->next()) !== false) { + while (($row = $iterator->next()) !== false) { $this->assertEquals(1, count($row)); $this->assertTrue(is_array($row[0])); + if ($rowNum == 0) { $this->assertEquals(1, $row[0]['id']); $this->assertEquals('romanb', $row[0]['name']); @@ -733,17 +778,22 @@ class ArrayHydratorTest extends HydrationTestCase $this->assertEquals(2, $row[0]['id']); $this->assertEquals('jwage', $row[0]['name']); } + ++$rowNum; } } /** + * SELECT PARTIAL u.{id, name} + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * * @group DDC-644 + * @dataProvider provideDataForUserEntityResult */ - public function testSkipUnknownColumns() + public function testSkipUnknownColumns($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); @@ -753,24 +803,30 @@ class ArrayHydratorTest extends HydrationTestCase 'u__id' => '1', 'u__name' => 'romanb', 'foo' => 'bar', // unknown! - ), + ), ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(1, count($result)); + $this->assertArrayHasKey('id', $result[0]); + $this->assertArrayHasKey('name', $result[0]); + $this->assertArrayNotHasKey('foo', $result[0]); } /** + * SELECT PARTIAL u.{id, status}, UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * * @group DDC-1358 + * @dataProvider provideDataForUserEntityResult */ - public function testMissingIdForRootEntity() + public function testMissingIdForRootEntity($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'nameUpper'); @@ -782,28 +838,27 @@ class ArrayHydratorTest extends HydrationTestCase '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); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(4, count($result), "Should hydrate four results."); @@ -812,19 +867,24 @@ class ArrayHydratorTest extends HydrationTestCase $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]); + $this->assertEquals(array('id' => 1, 'status' => 'developer'), $result[0][$userEntityKey]); + $this->assertNull($result[1][$userEntityKey]); + $this->assertEquals(array('id' => 2, 'status' => 'developer'), $result[2][$userEntityKey]); + $this->assertNull($result[3][$userEntityKey]); } /** + * SELECT PARTIAL u.{id, status}, UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * INDEX BY u.id + * * @group DDC-1385 + * @dataProvider provideDataForUserEntityResult */ - public function testIndexByAndMixedResult() + public function testIndexByAndMixedResult($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'nameUpper'); @@ -837,23 +897,24 @@ class ArrayHydratorTest extends HydrationTestCase 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); + $this->assertTrue(isset($result[1])); - $this->assertEquals(1, $result[1][0]['id']); + $this->assertEquals(1, $result[1][$userEntityKey]['id']); + $this->assertTrue(isset($result[2])); - $this->assertEquals(2, $result[2][0]['id']); + $this->assertEquals(2, $result[2][$userEntityKey]['id']); } } \ 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 268118d72..c3d6a9412 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php @@ -15,14 +15,42 @@ require_once __DIR__ . '/../../TestInit.php'; class ObjectHydratorTest extends HydrationTestCase { + public function provideDataForUserEntityResult() + { + return array( + array(0), + array('user'), + ); + } + + public function provideDataForMultipleRootEntityResult() + { + return array( + array(0, 0), + array('user', 0), + array(0, 'article'), + array('user', 'article'), + ); + } + + public function provideDataForProductEntityResult() + { + return array( + array(0), + array('product'), + ); + } + /** - * SELECT PARTIAL u.{id,name} + * SELECT PARTIAL u.{id,name} * FROM Doctrine\Tests\Models\CMS\CmsUser u + * + * @dataProvider provideDataForUserEntityResult */ - public function testSimpleEntityScalarFieldsQuery() + public function testSimpleEntityScalarFieldsQuery($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); @@ -43,26 +71,28 @@ class ObjectHydratorTest extends HydrationTestCase $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); - + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0]); $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1]); - + $this->assertEquals(1, $result[0]->id); $this->assertEquals('romanb', $result[0]->name); + $this->assertEquals(2, $result[1]->id); $this->assertEquals('jwage', $result[1]->name); } /** - * SELECT PARTIAL u.{id,name} + * SELECT PARTIAL u.{id,name} * FROM Doctrine\Tests\Models\CMS\CmsUser u - * + * * @group DDC-644 + * @dataProvider provideDataForUserEntityResult */ - public function testSkipUnknownColumns() + public function testSkipUnknownColumns($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); @@ -83,17 +113,18 @@ class ObjectHydratorTest extends HydrationTestCase } /** - * SELECT u.id, - * u.name + * SELECT u.id, u.name * FROM Doctrine\Tests\Models\CMS\CmsUser u + * + * @dataProvider provideDataForUserEntityResult */ - public function testScalarQueryWithoutResultVariables() + public function testScalarQueryWithoutResultVariables($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addScalarResult('sclr0', 'id'); $rsm->addScalarResult('sclr1', 'name'); - + // Faked result set $resultSet = array( array( @@ -111,27 +142,28 @@ class ObjectHydratorTest extends HydrationTestCase $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); - + $this->assertInternalType('array', $result[0]); $this->assertInternalType('array', $result[1]); - + $this->assertEquals(1, $result[0]['id']); $this->assertEquals('romanb', $result[0]['name']); + $this->assertEquals(2, $result[1]['id']); $this->assertEquals('jwage', $result[1]['name']); } - + /** - * SELECT PARTIAL u.{id, name} - * PARTIAL a.{id, topic} - * FROM Doctrine\Tests\Models\CMS\CmsUser u, - * Doctrine\Tests\Models\CMS\CmsArticle a + * SELECT PARTIAL u.{id, name}, PARTIAL a.{id, topic} + * FROM Doctrine\Tests\Models\CMS\CmsUser u, Doctrine\Tests\Models\CMS\CmsArticle a + * + * @dataProvider provideDataForMultipleRootEntityResult */ - public function testSimpleMultipleRootEntityQuery() + public function testSimpleMultipleRootEntityQuery($userEntityKey, $articleEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsArticle', 'a'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsArticle', 'a', $articleEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); $rsm->addFieldResult('a', 'a__id', 'id'); @@ -166,22 +198,27 @@ class ObjectHydratorTest extends HydrationTestCase $this->assertEquals(1, $result[0]->id); $this->assertEquals('romanb', $result[0]->name); + $this->assertEquals(1, $result[1]->id); $this->assertEquals('Cool things.', $result[1]->topic); + $this->assertEquals(2, $result[2]->id); $this->assertEquals('jwage', $result[2]->name); + $this->assertEquals(2, $result[3]->id); $this->assertEquals('Cool things II.', $result[3]->topic); } /** - * SELECT p + * SELECT p * FROM Doctrine\Tests\Models\ECommerce\ECommerceProduct p + * + * @dataProvider provideDataForProductEntityResult */ - public function testCreatesProxyForLazyLoadingWithForeignKeys() + public function testCreatesProxyForLazyLoadingWithForeignKeys($productEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\ECommerce\ECommerceProduct', 'p'); + $rsm->addEntityResult('Doctrine\Tests\Models\ECommerce\ECommerceProduct', 'p', $productEntityKey ?: null); $rsm->addFieldResult('p', 'p__id', 'id'); $rsm->addFieldResult('p', 'p__name', 'name'); $rsm->addMetaResult('p', 'p__shipping_id', 'shipping_id'); @@ -215,21 +252,21 @@ class ObjectHydratorTest extends HydrationTestCase $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(1, count($result)); - + $this->assertInstanceOf('Doctrine\Tests\Models\ECommerce\ECommerceProduct', $result[0]); } /** - * SELECT PARTIAL u.{id, status}, - * PARTIAL p.{phonenumber}, - * UPPER(u.name) nameUpper - * FROM User u + * SELECT PARTIAL u.{id, status}, PARTIAL p.{phonenumber}, UPPER(u.name) nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u * JOIN u.phonenumbers p + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryFetchJoin() + public function testMixedQueryFetchJoin($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( 'Doctrine\Tests\Models\CMS\CmsPhonenumber', 'p', @@ -269,42 +306,44 @@ class ObjectHydratorTest extends HydrationTestCase $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); - + $this->assertInternalType('array', $result); $this->assertInternalType('array', $result[0]); $this->assertInternalType('array', $result[1]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->phonenumbers); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][0]->phonenumbers[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][0]->phonenumbers[1]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][0]); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][0]->phonenumbers); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][$userEntityKey]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][$userEntityKey]->phonenumbers); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][$userEntityKey]->phonenumbers[0]); + + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][$userEntityKey]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][$userEntityKey]->phonenumbers); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][$userEntityKey]->phonenumbers[1]); // first user => 2 phonenumbers - $this->assertEquals(2, count($result[0][0]->phonenumbers)); + $this->assertEquals(2, count($result[0][$userEntityKey]->phonenumbers)); $this->assertEquals('ROMANB', $result[0]['nameUpper']); - + // second user => 1 phonenumber - $this->assertEquals(1, count($result[1][0]->phonenumbers)); + $this->assertEquals(1, count($result[1][$userEntityKey]->phonenumbers)); $this->assertEquals('JWAGE', $result[1]['nameUpper']); - $this->assertEquals(42, $result[0][0]->phonenumbers[0]->phonenumber); - $this->assertEquals(43, $result[0][0]->phonenumbers[1]->phonenumber); - $this->assertEquals(91, $result[1][0]->phonenumbers[0]->phonenumber); + $this->assertEquals(42, $result[0][$userEntityKey]->phonenumbers[0]->phonenumber); + $this->assertEquals(43, $result[0][$userEntityKey]->phonenumbers[1]->phonenumber); + $this->assertEquals(91, $result[1][$userEntityKey]->phonenumbers[0]->phonenumber); } /** - * SELECT PARTIAL u.{id, status}, - * COUNT(p.phonenumber) numPhones + * SELECT PARTIAL u.{id, status}, COUNT(p.phonenumber) numPhones * FROM User u - * JOIN u.phonenumbers p + * JOIN u.phonenumbers p * GROUP BY u.id + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryNormalJoin() + public function testMixedQueryNormalJoin($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'numPhones'); @@ -329,33 +368,33 @@ class ObjectHydratorTest extends HydrationTestCase $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); - + $this->assertInternalType('array', $result); $this->assertInternalType('array', $result[0]); $this->assertInternalType('array', $result[1]); // first user => 2 phonenumbers $this->assertEquals(2, $result[0]['numPhones']); - + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][$userEntityKey]); + // second user => 1 phonenumber $this->assertEquals(1, $result[1]['numPhones']); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][$userEntityKey]); } /** - * SELECT u, - * p, - * UPPER(u.name) nameUpper - * FROM User u + * SELECT u, p, UPPER(u.name) nameUpper + * FROM User u * INDEX BY u.id - * JOIN u.phonenumbers p + * JOIN u.phonenumbers p * INDEX BY p.phonenumber + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryFetchJoinCustomIndex() + public function testMixedQueryFetchJoinCustomIndex($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( 'Doctrine\Tests\Models\CMS\CmsPhonenumber', 'p', @@ -398,7 +437,7 @@ class ObjectHydratorTest extends HydrationTestCase $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); - + $this->assertInternalType('array', $result); $this->assertInternalType('array', $result[1]); $this->assertInternalType('array', $result[2]); @@ -407,43 +446,45 @@ class ObjectHydratorTest extends HydrationTestCase $this->assertEquals('ROMANB', $result[1]['nameUpper']); $this->assertEquals('JWAGE', $result[2]['nameUpper']); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[2][0]); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][0]->phonenumbers); - + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][$userEntityKey]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[2][$userEntityKey]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][$userEntityKey]->phonenumbers); + // first user => 2 phonenumbers. notice the custom indexing by user id - $this->assertEquals(2, count($result[1][0]->phonenumbers)); - + $this->assertEquals(2, count($result[1][$userEntityKey]->phonenumbers)); + // second user => 1 phonenumber. notice the custom indexing by user id - $this->assertEquals(1, count($result[2][0]->phonenumbers)); - + $this->assertEquals(1, count($result[2][$userEntityKey]->phonenumbers)); + // test the custom indexing of the phonenumbers - $this->assertTrue(isset($result[1][0]->phonenumbers['42'])); - $this->assertTrue(isset($result[1][0]->phonenumbers['43'])); - $this->assertTrue(isset($result[2][0]->phonenumbers['91'])); + $this->assertTrue(isset($result[1][$userEntityKey]->phonenumbers['42'])); + $this->assertTrue(isset($result[1][$userEntityKey]->phonenumbers['43'])); + $this->assertTrue(isset($result[2][$userEntityKey]->phonenumbers['91'])); } /** - * select u, p, upper(u.name) nameUpper, a - * from User u - * join u.phonenumbers p - * join u.articles a + * SELECT u, p, UPPER(u.name) nameUpper, a + * FROM User u + * JOIN u.phonenumbers p + * JOIN u.articles a + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryMultipleFetchJoin() + public function testMixedQueryMultipleFetchJoin($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', - 'p', - 'u', - 'phonenumbers' + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' ); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsArticle', - 'a', - 'u', - 'articles' + 'Doctrine\Tests\Models\CMS\CmsArticle', + 'a', + 'u', + 'articles' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -462,15 +503,15 @@ class ObjectHydratorTest extends HydrationTestCase 'p__phonenumber' => '42', 'a__id' => '1', 'a__topic' => 'Getting things done!' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', 'a__id' => '1', 'a__topic' => 'Getting things done!' - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', @@ -478,15 +519,15 @@ class ObjectHydratorTest extends HydrationTestCase 'p__phonenumber' => '42', 'a__id' => '2', 'a__topic' => 'ZendCon' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', 'a__id' => '2', 'a__topic' => 'ZendCon' - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', @@ -494,70 +535,72 @@ class ObjectHydratorTest extends HydrationTestCase 'p__phonenumber' => '91', 'a__id' => '3', 'a__topic' => 'LINQ' - ), - array( + ), + array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', 'p__phonenumber' => '91', 'a__id' => '4', 'a__topic' => 'PHP6' - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $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)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); $this->assertTrue(is_array($result[0])); $this->assertTrue(is_array($result[1])); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->phonenumbers); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][0]->phonenumbers[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][0]->phonenumbers[1]); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->articles); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][0]->articles[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][0]->articles[1]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][0]); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][0]->phonenumbers); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[1][0]->phonenumbers[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][0]->articles[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][0]->articles[1]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][$userEntityKey]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][$userEntityKey]->phonenumbers); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][$userEntityKey]->phonenumbers[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][$userEntityKey]->phonenumbers[1]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][$userEntityKey]->articles); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][$userEntityKey]->articles[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][$userEntityKey]->articles[1]); + + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][$userEntityKey]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][$userEntityKey]->phonenumbers); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[1][$userEntityKey]->phonenumbers[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][$userEntityKey]->articles[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][$userEntityKey]->articles[1]); } /** - * select u, p, upper(u.name) nameUpper, a, c - * c.id, c.topic - * from User u - * join u.phonenumbers p - * join u.articles a - * left join a.comments c + * SELECT u, p, UPPER(u.name) nameUpper, a, c + * FROM User u + * JOIN u.phonenumbers p + * JOIN u.articles a + * LEFT JOIN a.comments c + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryMultipleDeepMixedFetchJoin() + public function testMixedQueryMultipleDeepMixedFetchJoin($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', - 'p', - 'u', - 'phonenumbers' + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' ); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsArticle', - 'a', - 'u', - 'articles' + 'Doctrine\Tests\Models\CMS\CmsArticle', + 'a', + 'u', + 'articles' ); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsComment', - 'c', - 'a', - 'comments' + 'Doctrine\Tests\Models\CMS\CmsComment', + 'c', + 'a', + 'comments' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -580,8 +623,8 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'Getting things done!', 'c__id' => '1', 'c__topic' => 'First!' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', @@ -590,7 +633,7 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'Getting things done!', 'c__id' => '1', 'c__topic' => 'First!' - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', @@ -600,8 +643,8 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'ZendCon', 'c__id' => null, 'c__topic' => null - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', @@ -610,7 +653,7 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'ZendCon', 'c__id' => null, 'c__topic' => null - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', @@ -620,8 +663,8 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'LINQ', 'c__id' => null, 'c__topic' => null - ), - array( + ), + array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', @@ -630,43 +673,50 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'PHP6', 'c__id' => null, 'c__topic' => null - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $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)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); $this->assertTrue(is_array($result[0])); $this->assertTrue(is_array($result[1])); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][$userEntityKey]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][$userEntityKey]); + // phonenumbers - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->phonenumbers); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][0]->phonenumbers[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][0]->phonenumbers[1]); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][0]->phonenumbers); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[1][0]->phonenumbers[0]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][$userEntityKey]->phonenumbers); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][$userEntityKey]->phonenumbers[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][$userEntityKey]->phonenumbers[1]); + + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][$userEntityKey]->phonenumbers); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[1][$userEntityKey]->phonenumbers[0]); + // articles - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->articles); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][0]->articles[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][0]->articles[1]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][0]->articles[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][0]->articles[1]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][$userEntityKey]->articles); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][$userEntityKey]->articles[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][$userEntityKey]->articles[1]); + + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][$userEntityKey]->articles[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][$userEntityKey]->articles[1]); + // article comments - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->articles[0]->comments); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsComment', $result[0][0]->articles[0]->comments[0]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][$userEntityKey]->articles[0]->comments); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsComment', $result[0][$userEntityKey]->articles[0]->comments[0]); + // empty comment collections - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->articles[1]->comments); - $this->assertEquals(0, count($result[0][0]->articles[1]->comments)); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][0]->articles[0]->comments); - $this->assertEquals(0, count($result[1][0]->articles[0]->comments)); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][0]->articles[1]->comments); - $this->assertEquals(0, count($result[1][0]->articles[1]->comments)); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][$userEntityKey]->articles[1]->comments); + $this->assertEquals(0, count($result[0][$userEntityKey]->articles[1]->comments)); + + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][$userEntityKey]->articles[0]->comments); + $this->assertEquals(0, count($result[1][$userEntityKey]->articles[0]->comments)); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][$userEntityKey]->articles[1]->comments); + $this->assertEquals(0, count($result[1][$userEntityKey]->articles[1]->comments)); } /** @@ -692,10 +742,10 @@ class ObjectHydratorTest extends HydrationTestCase $rsm = new ResultSetMapping; $rsm->addEntityResult('Doctrine\Tests\Models\Forum\ForumCategory', 'c'); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\Forum\ForumBoard', - 'b', - 'c', - 'boards' + 'Doctrine\Tests\Models\Forum\ForumBoard', + 'b', + 'c', + 'boards' ); $rsm->addFieldResult('c', 'c__id', 'id'); $rsm->addFieldResult('c', 'c__position', 'position'); @@ -712,15 +762,15 @@ class ObjectHydratorTest extends HydrationTestCase 'b__id' => '1', 'b__position' => '0', //'b__category_id' => '1' - ), - array( + ), + array( 'c__id' => '2', 'c__position' => '0', 'c__name' => 'Second', 'b__id' => '2', 'b__position' => '0', //'b__category_id' => '2' - ), + ), array( 'c__id' => '1', 'c__position' => '0', @@ -728,50 +778,62 @@ class ObjectHydratorTest extends HydrationTestCase 'b__id' => '3', 'b__position' => '1', //'b__category_id' => '1' - ), - array( + ), + array( 'c__id' => '1', 'c__position' => '0', 'c__name' => 'First', 'b__id' => '4', 'b__position' => '2', //'b__category_id' => '1' - ) - ); + ) + ); - $stmt = new HydratorMockStatement($resultSet); + $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)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); + $this->assertInstanceOf('Doctrine\Tests\Models\Forum\ForumCategory', $result[0]); $this->assertInstanceOf('Doctrine\Tests\Models\Forum\ForumCategory', $result[1]); + $this->assertTrue($result[0] !== $result[1]); + $this->assertEquals(1, $result[0]->getId()); $this->assertEquals(2, $result[1]->getId()); + $this->assertTrue(isset($result[0]->boards)); $this->assertEquals(3, count($result[0]->boards)); + $this->assertTrue(isset($result[1]->boards)); $this->assertEquals(1, count($result[1]->boards)); } - public function testChainedJoinWithEmptyCollections() + /** + * SELECT PARTIAL u.{id, status}, PARTIAL a.{id, topic}, PARTIAL c.{id, topic} + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * LEFT JOIN u.articles a + * LEFT JOIN a.comments c + * + * @dataProvider provideDataForUserEntityResult + */ + public function testChainedJoinWithEmptyCollections($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsArticle', - 'a', - 'u', - 'articles' + 'Doctrine\Tests\Models\CMS\CmsArticle', + 'a', + 'u', + 'articles' ); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsComment', - 'c', - 'a', - 'comments' + 'Doctrine\Tests\Models\CMS\CmsComment', + 'c', + 'a', + 'comments' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -790,38 +852,43 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => null, 'c__id' => null, 'c__topic' => null - ), - array( + ), + array( 'u__id' => '2', 'u__status' => 'developer', 'a__id' => null, 'a__topic' => null, 'c__id' => null, 'c__topic' => null - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $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)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0]); $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1]); + $this->assertEquals(0, $result[0]->articles->count()); $this->assertEquals(0, $result[1]->articles->count()); } /** - * DQL: select partial u.{id,status}, a.id, a.topic, c.id as cid, c.topic as ctopic from CmsUser u left join u.articles a left join a.comments c + * SELECT PARTIAL u.{id,status}, a.id, a.topic, c.id as cid, c.topic as ctopic + * FROM CmsUser u + * LEFT JOIN u.articles a + * LEFT JOIN a.comments c * * @group bubu + * @dataProvider provideDataForUserEntityResult */ - /*public function testChainedJoinWithScalars() + /*public function testChainedJoinWithScalars($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('a__id', 'id'); @@ -839,7 +906,7 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'The First', 'c__id' => '1', 'c__topic' => 'First Comment' - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', @@ -847,51 +914,39 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'The First', 'c__id' => '2', 'c__topic' => 'Second Comment' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'a__id' => '42', 'a__topic' => 'The Answer', 'c__id' => null, 'c__topic' => null - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $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)); - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + \Doctrine\Common\Util\Debug::dump($result, 3); - $this->assertEquals(3, count($result)); + $this->assertEquals(1, count($result)); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); // User object - $this->assertEquals(1, $result[0]['id']); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][$userEntityKey]); // User object + $this->assertEquals(42, $result[0]['id']); $this->assertEquals('The First', $result[0]['topic']); $this->assertEquals(1, $result[0]['cid']); $this->assertEquals('First Comment', $result[0]['ctopic']); - - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][0]); // Same User object - $this->assertEquals(1, $result[1]['id']); // duplicated - $this->assertEquals('The First', $result[1]['topic']); // duplicated - $this->assertEquals(2, $result[1]['cid']); - $this->assertEquals('Second Comment', $result[1]['ctopic']); - - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[2][0]); // Same User object - $this->assertEquals(42, $result[2]['id']); - $this->assertEquals('The Answer', $result[2]['topic']); - $this->assertNull($result[2]['cid']); - $this->assertNull($result[2]['ctopic']); - - $this->assertTrue($result[0][0] === $result[1][0]); - $this->assertTrue($result[1][0] === $result[2][0]); - $this->assertTrue($result[0][0] === $result[2][0]); }*/ - public function testResultIteration() + /** + * @dataProvider provideDataForUserEntityResult + */ + public function testResultIteration($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); @@ -900,23 +955,22 @@ class ObjectHydratorTest extends HydrationTestCase array( 'u__id' => '1', 'u__name' => 'romanb' - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage' - ) - ); - - - $stmt = new HydratorMockStatement($resultSet); - $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); + ) + ); + $stmt = new HydratorMockStatement($resultSet); + $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); $iterableResult = $hydrator->iterate($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $rowNum = 0; - $rowNum = 0; while (($row = $iterableResult->next()) !== false) { $this->assertEquals(1, count($row)); $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $row[0]); + if ($rowNum == 0) { $this->assertEquals(1, $row[0]->id); $this->assertEquals('romanb', $row[0]->name); @@ -924,21 +978,24 @@ class ObjectHydratorTest extends HydrationTestCase $this->assertEquals(2, $row[0]->id); $this->assertEquals('jwage', $row[0]->name); } + ++$rowNum; } } /** - * This issue tests if, with multiple joined multiple-valued collections the hydration is done correctly. + * Checks if multiple joined multiple-valued collections is hydrated correctly. * - * User x Phonenumbers x Groups blow up the resultset quite a bit, however the hydration should correctly assemble those. + * SELECT PARTIAL u.{id, status}, PARTIAL g.{id, name}, PARTIAL p.{phonenumber} + * FROM Doctrine\Tests\Models\CMS\CmsUser u * * @group DDC-809 + * @dataProvider provideDataForUserEntityResult */ - public function testManyToManyHydration() + public function testManyToManyHydration($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); $rsm->addJoinedEntityResult('Doctrine\Tests\Models\CMS\CmsGroup', 'g', 'u', 'groups'); @@ -955,106 +1012,112 @@ class ObjectHydratorTest extends HydrationTestCase 'g__id' => '3', 'g__name' => 'TestGroupB', 'p__phonenumber' => 1111, - ), + ), array( 'u__id' => '1', 'u__name' => 'romanb', 'g__id' => '5', 'g__name' => 'TestGroupD', 'p__phonenumber' => 1111, - ), + ), array( 'u__id' => '1', 'u__name' => 'romanb', 'g__id' => '3', 'g__name' => 'TestGroupB', 'p__phonenumber' => 2222, - ), + ), array( 'u__id' => '1', 'u__name' => 'romanb', 'g__id' => '5', 'g__name' => 'TestGroupD', 'p__phonenumber' => 2222, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '2', 'g__name' => 'TestGroupA', 'p__phonenumber' => 3333, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '3', 'g__name' => 'TestGroupB', 'p__phonenumber' => 3333, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '4', 'g__name' => 'TestGroupC', 'p__phonenumber' => 3333, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '5', 'g__name' => 'TestGroupD', 'p__phonenumber' => 3333, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '2', 'g__name' => 'TestGroupA', 'p__phonenumber' => 4444, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '3', 'g__name' => 'TestGroupB', 'p__phonenumber' => 4444, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '4', 'g__name' => 'TestGroupC', 'p__phonenumber' => 4444, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '5', 'g__name' => 'TestGroupD', 'p__phonenumber' => 4444, - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $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)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); + $this->assertContainsOnly('Doctrine\Tests\Models\CMS\CmsUser', $result); + $this->assertEquals(2, count($result[0]->groups)); $this->assertEquals(2, count($result[0]->phonenumbers)); + $this->assertEquals(4, count($result[1]->groups)); $this->assertEquals(2, count($result[1]->phonenumbers)); } /** + * SELECT PARTIAL u.{id, status}, UPPER(u.name) as nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * * @group DDC-1358 + * @dataProvider provideDataForUserEntityResult */ - public function testMissingIdForRootEntity() + public function testMissingIdForRootEntity($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'nameUpper'); @@ -1066,28 +1129,27 @@ class ObjectHydratorTest extends HydrationTestCase '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); + $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)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(4, count($result), "Should hydrate four results."); @@ -1096,25 +1158,30 @@ class ObjectHydratorTest extends HydrationTestCase $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]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][$userEntityKey]); + $this->assertNull($result[1][$userEntityKey]); + + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[2][$userEntityKey]); + $this->assertNull($result[3][$userEntityKey]); } /** + * SELECT PARTIAL u.{id, status}, PARTIAL p.{phonenumber}, UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * LEFT JOIN u.phonenumbers u + * * @group DDC-1358 - * @return void + * @dataProvider provideDataForUserEntityResult */ - public function testMissingIdForCollectionValuedChildEntity() + public function testMissingIdForCollectionValuedChildEntity($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', - 'p', - 'u', - 'phonenumbers' + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -1129,49 +1196,54 @@ class ObjectHydratorTest extends HydrationTestCase '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); + $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)); + $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()); + + $this->assertEquals(1, $result[0][$userEntityKey]->phonenumbers->count()); + $this->assertEquals(1, $result[1][$userEntityKey]->phonenumbers->count()); } /** + * SELECT PARTIAL u.{id, status}, PARTIAL a.{id, city}, UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * JOIN u.address a + * * @group DDC-1358 + * @dataProvider provideDataForUserEntityResult */ - public function testMissingIdForSingleValuedChildEntity() + public function testMissingIdForSingleValuedChildEntity($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsAddress', - 'a', - 'u', - 'address' + 'Doctrine\Tests\Models\CMS\CmsAddress', + 'a', + 'u', + 'address' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -1199,23 +1271,28 @@ class ObjectHydratorTest extends HydrationTestCase ), ); - $stmt = new HydratorMockStatement($resultSet); + $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)); + $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); + + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsAddress', $result[0][$userEntityKey]->address); + $this->assertNull($result[1][$userEntityKey]->address); } /** + * SELECT PARTIAL u.{id, status}, UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * INDEX BY u.id + * * @group DDC-1385 + * @dataProvider provideDataForUserEntityResult */ - public function testIndexByAndMixedResult() + public function testIndexByAndMixedResult($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'nameUpper'); @@ -1236,24 +1313,30 @@ class ObjectHydratorTest extends HydrationTestCase ), ); - $stmt = new HydratorMockStatement($resultSet); + $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)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); + $this->assertTrue(isset($result[1])); - $this->assertEquals(1, $result[1][0]->id); + $this->assertEquals(1, $result[1][$userEntityKey]->id); + $this->assertTrue(isset($result[2])); - $this->assertEquals(2, $result[2][0]->id); + $this->assertEquals(2, $result[2][$userEntityKey]->id); } /** + * SELECT UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * * @group DDC-1385 + * @dataProvider provideDataForUserEntityResult */ - public function testIndexByScalarsOnly() + public function testIndexByScalarsOnly($userEntityKey) { $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addScalarResult('sclr0', 'nameUpper'); $rsm->addIndexByScalar('sclr0'); @@ -1270,9 +1353,14 @@ class ObjectHydratorTest extends HydrationTestCase $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(array('ROMANB' => array('nameUpper' => 'ROMANB'), 'JWAGE' => array('nameUpper' => 'JWAGE')), $result); + $this->assertEquals( + array( + 'ROMANB' => array('nameUpper' => 'ROMANB'), + 'JWAGE' => array('nameUpper' => 'JWAGE') + ), + $result + ); } }