diff --git a/lib/Doctrine/Common/Collections/Collection.php b/lib/Doctrine/Common/Collections/Collection.php index 52989b490..882524b0c 100644 --- a/lib/Doctrine/Common/Collections/Collection.php +++ b/lib/Doctrine/Common/Collections/Collection.php @@ -8,7 +8,6 @@ #use \Countable; #use \IteratorAggregate; -#use \Serializable; #use \ArrayAccess; /** @@ -17,7 +16,8 @@ * * @author robo */ -class Doctrine_Common_Collections_Collection implements Countable, IteratorAggregate, Serializable, ArrayAccess { +class Doctrine_Common_Collections_Collection implements Countable, IteratorAggregate, ArrayAccess +{ /** * An array containing the entries of this collection. * This is the wrapped php array. @@ -89,10 +89,7 @@ class Doctrine_Common_Collections_Collection implements Countable, IteratorAggre } /** - * __isset() - * - * @param string $name - * @return boolean whether or not this object contains $name + * @see containsKey() */ public function __isset($key) { @@ -100,10 +97,7 @@ class Doctrine_Common_Collections_Collection implements Countable, IteratorAggre } /** - * __unset() - * - * @param string $key - * @return mixed + * @see remove() */ public function __unset($key) { @@ -113,10 +107,7 @@ class Doctrine_Common_Collections_Collection implements Countable, IteratorAggre /* ArrayAccess implementation */ /** - * Check if an offset exists. - * - * @param mixed $offset - * @return boolean Whether or not this object contains $offset + * @see containsKey() */ public function offsetExists($offset) { @@ -124,12 +115,7 @@ class Doctrine_Common_Collections_Collection implements Countable, IteratorAggre } /** - * Gets the element with the given key. - * - * Part of the ArrayAccess implementation. - * - * @param mixed $offset - * @return mixed + * @see get() */ public function offsetGet($offset) { @@ -137,13 +123,8 @@ class Doctrine_Common_Collections_Collection implements Countable, IteratorAggre } /** - * Part of the ArrayAccess implementation. - * - * sets $offset to $value - * @see set, __set - * @param mixed $offset - * @param mixed $value - * @return void + * @see add() + * @see set() */ public function offsetSet($offset, $value) { @@ -154,11 +135,7 @@ class Doctrine_Common_Collections_Collection implements Countable, IteratorAggre } /** - * Part of the ArrayAccess implementation. - * - * unset a given offset - * @see set, offsetSet, __set - * @param mixed $offset + * @see remove() */ public function offsetUnset($offset) { @@ -171,7 +148,7 @@ class Doctrine_Common_Collections_Collection implements Countable, IteratorAggre * Checks whether the collection contains a specific key/index. * * @param mixed $key The key to check for. - * @return boolean + * @return boolean TRUE if the given key/index exists, FALSE otherwise. */ public function containsKey($key) { @@ -185,7 +162,8 @@ class Doctrine_Common_Collections_Collection implements Countable, IteratorAggre * For objects this means reference equality. * * @param mixed $element - * @return boolean + * @return boolean TRUE if the given element is contained in the collection, + * FALSE otherwise. */ public function contains($element) { @@ -196,9 +174,9 @@ class Doctrine_Common_Collections_Collection implements Countable, IteratorAggre * Tests for the existance of an element that satisfies the given predicate. * * @param function $func - * @return boolean + * @return boolean TRUE if the predicate is TRUE for at least one element, FALSe otherwise. */ - public function exists($func) { + public function exists(Closure $func) { foreach ($this->_data as $key => $element) if ($func($key, $element)) return true; @@ -213,7 +191,7 @@ class Doctrine_Common_Collections_Collection implements Countable, IteratorAggre */ public function containsAll($otherColl) { - //... + throw new Doctrine_Exception("Not yet implemented."); } /** @@ -343,7 +321,7 @@ class Doctrine_Common_Collections_Collection implements Countable, IteratorAggre * * @param function $func */ - public function map($func) + public function map(Closure $func) { return new Doctrine_Common_Collections_Collection(array_map($func, $this->_data)); } @@ -354,7 +332,7 @@ class Doctrine_Common_Collections_Collection implements Countable, IteratorAggre * * @param function $func */ - public function filter($func) + public function filter(Closure $func) { return new Doctrine_Common_Collections_Collection(array_filter($this->_data, $func)); } @@ -376,39 +354,5 @@ class Doctrine_Common_Collections_Collection implements Countable, IteratorAggre { $this->_data = array(); } - - /* Serializable implementation */ - - /** - * Serializes the collection. - * This method is automatically called when the Collection is serialized. - * - * Part of the implementation of the Serializable interface. - * - * @return array - */ - public function serialize() - { - $vars = get_object_vars($this); - - //TODO - - return serialize($vars); - } - - /** - * Reconstitutes the collection object from it's serialized form. - * This method is automatically called everytime the Collection object is unserialized. - * - * Part of the implementation of the Serializable interface. - * - * @param string $serialized The serialized data - * - * @return void - */ - public function unserialize($serialized) - { - //TODO - } } diff --git a/lib/Doctrine/DBAL/Driver/PDOMySql/Driver.php b/lib/Doctrine/DBAL/Driver/PDOMySql/Driver.php index 855985d21..32a1122d9 100644 --- a/lib/Doctrine/DBAL/Driver/PDOMySql/Driver.php +++ b/lib/Doctrine/DBAL/Driver/PDOMySql/Driver.php @@ -6,14 +6,24 @@ class Doctrine_DBAL_Driver_PDOMySql_Driver implements Doctrine_DBAL_Driver { - + /** + * Attempts to establish a connection with the underlying driver. + * + * @param array $params + * @param string $username + * @param string $password + * @param array $driverOptions + * @return Doctrine\DBAL\Driver\Connection + */ public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) { - return new Doctrine_DBAL_Driver_PDOConnection( + $conn = new Doctrine_DBAL_Driver_PDOConnection( $this->_constructPdoDsn($params), $username, $password, $driverOptions); + $conn->setAttribute(PDO::ATTR_AUTOCOMMIT, false); + return $conn; } /** @@ -54,4 +64,3 @@ class Doctrine_DBAL_Driver_PDOMySql_Driver implements Doctrine_DBAL_Driver } -?> \ No newline at end of file diff --git a/lib/Doctrine/ORM/Collection.php b/lib/Doctrine/ORM/Collection.php index f33854f0f..ae0632c8f 100644 --- a/lib/Doctrine/ORM/Collection.php +++ b/lib/Doctrine/ORM/Collection.php @@ -24,12 +24,8 @@ /** * A persistent collection wrapper. * - * A collection object is strongly typed in the sense that it can only contain - * entities of a specific type or one of it's subtypes. A collection object is - * basically a wrapper around an ordinary php array and just like a php array - * it can have List or Map semantics. - * - * A collection of entities represents only the associations (links) to those entities. + * A PersistentCollection represents a collection of entities. Collections of + * entities represent only the associations (links) to those entities. * That means, if the collection is part of a many-many mapping and you remove * entities from the collection, only the links in the xref table are removed (on flush). * Similarly, if you remove entities from a collection that is part of a one-many @@ -46,14 +42,14 @@ * @author Roman Borschel * @todo Rename to PersistentCollection */ -class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection +final class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection { /** * The base type of the collection. * * @var string */ - protected $_entityBaseType; + private $_entityBaseType; /** * A snapshot of the collection at the moment it was fetched from the database. @@ -61,14 +57,14 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection * * @var array */ - protected $_snapshot = array(); + private $_snapshot = array(); /** * The entity that owns this collection. * - * @var Doctrine\ORM\Entity + * @var object */ - protected $_owner; + private $_owner; /** * The association mapping the collection belongs to. @@ -76,21 +72,21 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection * * @var Doctrine\ORM\Mapping\AssociationMapping */ - protected $_association; + private $_association; /** * The name of the field that is used for collection key mapping. * * @var string */ - protected $_keyField; + private $_keyField; /** * The EntityManager that manages the persistence of the collection. * * @var Doctrine\ORM\EntityManager */ - protected $_em; + private $_em; /** * The name of the field on the target entities that points to the owner @@ -98,7 +94,7 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection * * @var string */ - protected $_backRefFieldName; + private $_backRefFieldName; /** * Hydration flag. @@ -106,7 +102,7 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection * @var boolean * @see _setHydrationFlag() */ - protected $_hydrationFlag; + private $_hydrationFlag; /** * Creates a new persistent collection. @@ -115,7 +111,6 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection { $this->_entityBaseType = $entityBaseType; $this->_em = $em; - if ($keyField !== null) { if ( ! $this->_em->getClassMetadata($entityBaseType)->hasField($keyField)) { throw new Doctrine_Exception("Invalid field '$keyField' can't be uses as key."); @@ -124,17 +119,6 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection } } - /** - * setData - * - * @param array $data - * @todo Remove? - */ - public function setData(array $data) - { - $this->_data = $data; - } - /** * INTERNAL: Sets the key column for this collection * @@ -161,7 +145,8 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection * INTERNAL: * Sets the collection owner. Used (only?) during hydration. * - * @return void + * @param object $entity + * @param AssociationMapping $relation */ public function _setOwner($entity, Doctrine_ORM_Mapping_AssociationMapping $relation) { @@ -184,7 +169,7 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection * INTERNAL: * Gets the collection owner. * - * @return Doctrine\ORM\Entity + * @return object */ public function _getOwner() { @@ -196,6 +181,7 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection * * @param mixed $key * @return boolean + * @override */ public function remove($key) { @@ -205,8 +191,9 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection /*if ($this->_association->isOneToMany() && $this->_association->shouldDeleteOrphans()) { $this->_em->delete($removed); }*/ - - return parent::remove($key); + $removed = parent::remove($key); + $this->_changed(); + return $removed; } /** @@ -215,13 +202,13 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection * * @param integer $key * @param mixed $value - * @return void + * @override */ public function set($key, $value) { parent::set($key, $value); //TODO: Register collection as dirty with the UoW if necessary - $this->_changed(); + if ( ! $this->_hydrationFlag) $this->_changed(); } /** @@ -230,10 +217,11 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection * @param mixed $value * @param string $key * @return boolean + * @override */ - public function add($value, $key = null) + public function add($value) { - $result = parent::add($value, $key); + $result = parent::add($value); if ( ! $result) return $result; // EARLY EXIT if ($this->_hydrationFlag) { @@ -287,10 +275,6 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection * Snapshots are used for diff processing, for example * when a fetched collection has three elements, then two of those * are being removed the diff would contain one element. - * - * Collection::save() attaches the diff with the help of last snapshot. - * - * @return void */ public function _takeSnapshot() { @@ -299,9 +283,9 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection /** * INTERNAL: - * Returns the data of the last snapshot. + * Returns the last snapshot of the elements in the collection. * - * @return array returns the data in last snapshot + * @return array The last snapshot of the elements. */ public function _getSnapshot() { @@ -366,7 +350,7 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection /** * INTERNAL: Gets the association mapping of the collection. * - * @return Doctrine::ORM::Mapping::AssociationMapping + * @return Doctrine\ORM\Mapping\AssociationMapping */ public function getMapping() { @@ -375,8 +359,6 @@ class Doctrine_ORM_Collection extends Doctrine_Common_Collections_Collection /** * Clears the collection. - * - * @return void */ public function clear() { diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 17e122ea9..80f615779 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -64,13 +64,12 @@ class Doctrine_ORM_EntityManager * The currently active EntityManager. Only one EntityManager can be active * at any time. * - * @var Doctrine::ORM::EntityManager + * @var Doctrine\ORM\EntityManager */ private static $_activeEm; /** - * The unique name of the EntityManager. The name is used to bind entity classes - * to certain EntityManagers. + * The unique name of the EntityManager. * * @var string */ @@ -79,14 +78,14 @@ class Doctrine_ORM_EntityManager /** * The used Configuration. * - * @var Configuration + * @var Doctrine\ORM\Configuration */ private $_config; /** * The database connection used by the EntityManager. * - * @var Connection + * @var Doctrine\DBAL\Connection */ private $_conn; @@ -98,7 +97,7 @@ class Doctrine_ORM_EntityManager private $_metadataFactory; /** - * The EntityPersister instances. + * The EntityPersister instances used to persist entity instances. * * @var array */ @@ -119,16 +118,16 @@ class Doctrine_ORM_EntityManager private $_flushMode = 'commit'; /** - * The unit of work used to coordinate object-level transactions. + * The UnitOfWork used to coordinate object-level transactions. * - * @var UnitOfWork + * @var Doctrine\ORM\UnitOfWork */ private $_unitOfWork; /** * The event manager that is the central point of the event system. * - * @var EventManager + * @var Doctrine\Common\EventManager */ private $_eventManager; @@ -139,6 +138,13 @@ class Doctrine_ORM_EntityManager */ private $_idGenerators = array(); + /** + * The maintained (cached) hydrators. One instance per type. + * + * @var array + */ + private $_hydrators = array(); + /** Whether the EntityManager is closed or not. */ private $_closed = false; @@ -224,7 +230,7 @@ class Doctrine_ORM_EntityManager /** * Returns the metadata for a class. * - * @return Doctrine_Metadata + * @return Doctrine\ORM\Mapping\ClassMetadata * @internal Performance-sensitive method. */ public function getClassMetadata($className) @@ -249,7 +255,7 @@ class Doctrine_ORM_EntityManager * Used to lazily create the id generator. * * @param string $generatorType - * @return void + * @return object */ protected function _createIdGenerator($generatorType) { @@ -349,6 +355,8 @@ class Doctrine_ORM_EntityManager /** * Flushes all changes to objects that have been queued up to now to the database. + * This effectively synchronizes the in-memory state of managed objects with the + * database. */ public function flush() { @@ -362,7 +370,7 @@ class Doctrine_ORM_EntityManager * * @param string $entityName * @param mixed $identifier - * @return Doctrine\ORM\Entity + * @return object */ public function find($entityName, $identifier) { @@ -569,6 +577,49 @@ class Doctrine_ORM_EntityManager { return self::$_activeEm === $this; } + + /** + * Gets a hydrator for the given hydration mode. + * + * @param $hydrationMode + */ + public function getHydrator($hydrationMode) + { + if ( ! isset($this->_hydrators[$hydrationMode])) { + switch ($hydrationMode) { + case Doctrine_ORM_Query::HYDRATE_OBJECT: + $this->_hydrators[$hydrationMode] = new Doctrine_ORM_Internal_Hydration_ObjectHydrator($this); + break; + case Doctrine_ORM_Query::HYDRATE_ARRAY: + $this->_hydrators[$hydrationMode] = new Doctrine_ORM_Internal_Hydration_ArrayHydrator($this); + break; + case Doctrine_ORM_Query::HYDRATE_SCALAR: + case Doctrine_ORM_Query::HYDRATE_SINGLE_SCALAR: + $this->_hydrators[$hydrationMode] = new Doctrine_ORM_Internal_Hydration_ScalarHydrator($this); + break; + case Doctrine_ORM_Query::HYDRATE_NONE: + $this->_hydrators[$hydrationMode] = new Doctrine_ORM_Internal_Hydration_NoneHydrator($this); + break; + default: + throw new Doctrine_Exception("No hydrator found for hydration mode '$hydrationMode'."); + } + } else if ($this->_hydrators[$hydrationMode] instanceof Closure) { + $this->_hydrators[$hydrationMode] = $this->_hydrators[$hydrationMode]($this); + } + return $this->_hydrators[$hydrationMode]; + } + + /** + * Sets a hydrator for a hydration mode. + * + * @param mixed $hydrationMode + * @param object $hydrator Either a hydrator instance or a closure that creates a + * hydrator instance. + */ + public function setHydrator($hydrationMode, $hydrator) + { + $this->_hydrators[$hydrationMode] = $hydrator; + } /** * Makes this EntityManager the currently active one. @@ -630,4 +681,3 @@ class Doctrine_ORM_EntityManager } } -?> \ No newline at end of file diff --git a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php index 0daaf9f05..cfd8070ab 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php @@ -19,14 +19,14 @@ * . */ -#namespace Doctrine::ORM::Internal::Hydration; +#namespace Doctrine\ORM\Internal\Hydration; /** * Base class for all hydrators (ok, we got only 1 currently). * * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 + * @link www.doctrine-project.org + * @since 2.0 * @version $Revision: 3192 $ * @author Konsta Vesterinen * @author Roman Borschel @@ -46,102 +46,318 @@ abstract class Doctrine_ORM_Internal_Hydration_AbstractHydrator */ protected $_queryComponents = array(); - /** - * @var array Table alias map. Keys are SQL aliases and values DQL aliases. - */ - protected $_tableAliasMap = array(); + /** @var array Table alias map. Keys are SQL aliases and values DQL aliases. */ + protected $_tableAliases = array(); - /** - * The current hydration mode. - */ - protected $_hydrationMode = Doctrine_ORM_Query::HYDRATE_OBJECT; - - protected $_nullObject; - + /** @var EntityManager The EntityManager instance. */ protected $_em; + /** @var UnitOfWork The UnitOfWork of the associated EntityManager. */ + protected $_uow; + + /** @var array The cache used during row-by-row hydration. */ + protected $_cache = array(); + + /** @var Statement The statement that provides the data to hydrate. */ + protected $_stmt; + + /** @var object The ParserResult instance that holds the necessary information for hydration. */ + protected $_parserResult; /** - * Constructor. + * Initializes a new instance of a class derived from AbstractHydrator. * - * @param Doctrine::ORM::EntityManager $em The EntityManager to use during hydration. + * @param Doctrine\ORM\EntityManager $em The EntityManager to use. */ public function __construct(Doctrine_ORM_EntityManager $em) { $this->_em = $em; - $this->_nullObject = Doctrine_ORM_Internal_Null::$INSTANCE; + $this->_uow = $em->getUnitOfWork(); } /** - * setHydrationMode + * Initiates a row-by-row hydration. * - * Defines the hydration process mode. - * - * @param integer $hydrationMode Doctrine processing mode to be used during hydration process. - * One of the Doctrine::HYDRATE_* constants. + * @param object $stmt + * @param object $parserResult + * @return IterableResult */ - public function setHydrationMode($hydrationMode) + public function iterate($stmt, $parserResult) { - $this->_hydrationMode = $hydrationMode; + $this->_stmt = $stmt; + $this->_prepare($parserResult); + return new Doctrine_ORM_Internal_Hydration_IterableResult($this); } /** - * setQueryComponents + * Hydrates all rows returned by the passed statement instance at once. * - * Defines the mapping components. - * - * @param array $queryComponents Query components. - */ - public function setQueryComponents(array $queryComponents) - { - $this->_queryComponents = $queryComponents; - } - - /** - * getQueryComponents - * - * Gets the mapping components. - * - * @return array Query components. - */ - public function getQueryComponents() - { - return $this->_queryComponents; - } - - /** - * setTableAliasMap - * - * Defines the table aliases. - * - * @param array $tableAliasMap Table aliases. - */ - public function setTableAliasMap(array $tableAliasMap) - { - $this->_tableAliasMap = $tableAliasMap; - } - - /** - * getTableAliasMap - * - * Returns all table aliases. - * - * @return array Table aliases as an array. - */ - public function getTableAliasMap() - { - return $this->_tableAliasMap; - } - - /** - * Processes data returned by statement object. - * - * This is method defines the core of Doctrine object/array population algorithm. - * - * @param mixed $stmt PDOStatement - * @param integer $hydrationMode Doctrine processing mode to be used during hydration process. - * One of the Doctrine::HYDRATE_* constants. + * @param object $stmt + * @param object $parserResult * @return mixed */ - abstract public function hydrateResultSet($parserResult); + public function hydrateAll($stmt, $parserResult) + { + $this->_stmt = $stmt; + $this->_prepare($parserResult); + $result = $this->_hydrateAll($parserResult); + $this->_cleanup(); + return $result; + } + + /** + * Hydrates a single row returned by the current statement instance during + * row-by-row hydration with {@link iterate()}. + * + * @return mixed + */ + public function hydrateRow() + { + $row = $this->_stmt->fetch(PDO::FETCH_ASSOC); + if ( ! $row) { + $this->_cleanup(); + return false; + } + $result = $this->_getRowContainer(); + $this->_hydrateRow($row, $this->_cache, $result); + return $result; + } + + /** + * Excutes one-time preparation tasks once each time hydration is started + * through {@link hydrateAll} or {@link iterate()}. + * + * @param object $parserResult + */ + protected function _prepare($parserResult) + { + $this->_queryComponents = $parserResult->getQueryComponents(); + $this->_tableAliases = $parserResult->getTableAliasMap(); + $this->_parserResult = $parserResult; + } + + /** + * Excutes one-time cleanup tasks at the end of a hydration that was initiated + * through {@link hydrateAll} or {@link iterate()}. + */ + protected function _cleanup() + { + $this->_parserResult = null; + $this->_stmt->closeCursor(); + $this->_stmt = null; + } + + /** + * Hydrates a single row from the current statement instance. + * + * Template method. + * + * @param array $data The row data. + * @param array $cache The cache to use. + * @param mixed $result The result to fill. + */ + protected function _hydrateRow(array &$data, array &$cache, &$result) + {} + + /** + * Hydrates all rows from the current statement instance at once. + * + * @param object $parserResult + */ + abstract protected function _hydrateAll($parserResult); + + /** + * Gets the row container used during row-by-row hydration through {@link iterate()}. + */ + abstract protected function _getRowContainer(); + + /** + * Processes a row of the result set. + * Used for identity hydration (HYDRATE_IDENTITY_OBJECT and HYDRATE_IDENTITY_ARRAY). + * Puts the elements of a result row into a new array, grouped by the class + * they belong to. The column names in the result set are mapped to their + * field names during this procedure as well as any necessary conversions on + * the values applied. + * + * @return array An array with all the fields (name => value) of the data row, + * grouped by their component (alias). + */ + protected function _gatherRowData(&$data, &$cache, &$id, &$nonemptyComponents) + { + $rowData = array(); + + foreach ($data as $key => $value) { + // Parse each column name only once. Cache the results. + if ( ! isset($cache[$key])) { + if ($this->_isIgnoredName($key)) continue; + + // Cache general information like the column name <-> field name mapping + $e = explode(Doctrine_ORM_Query_ParserRule::SQLALIAS_SEPARATOR, $key); + $columnName = array_pop($e); + $cache[$key]['dqlAlias'] = $this->_tableAliases[ + implode(Doctrine_ORM_Query_ParserRule::SQLALIAS_SEPARATOR, $e) + ]; + $classMetadata = $this->_queryComponents[$cache[$key]['dqlAlias']]['metadata']; + // check whether it's an aggregate value or a regular field + if (isset($this->_queryComponents[$cache[$key]['dqlAlias']]['agg'][$columnName])) { + $fieldName = $this->_queryComponents[$cache[$key]['dqlAlias']]['agg'][$columnName]; + $cache[$key]['isScalar'] = true; + } else { + $fieldName = $this->_lookupFieldName($classMetadata, $columnName); + $cache[$key]['isScalar'] = false; + $cache[$key]['type'] = $classMetadata->getTypeOfColumn($columnName); + } + + $cache[$key]['fieldName'] = $fieldName; + + // Cache identifier information + if ($classMetadata->isIdentifier($fieldName)) { + $cache[$key]['isIdentifier'] = true; + } else { + $cache[$key]['isIdentifier'] = false; + } + } + + $class = $this->_queryComponents[$cache[$key]['dqlAlias']]['metadata']; + $dqlAlias = $cache[$key]['dqlAlias']; + $fieldName = $cache[$key]['fieldName']; + + if ($cache[$key]['isScalar']) { + $rowData['scalars'][$fieldName] = $value; + continue; + } + + if ($cache[$key]['isIdentifier']) { + $id[$dqlAlias] .= '|' . $value; + } + + $rowData[$dqlAlias][$fieldName] = $cache[$key]['type']->convertToPHPValue($value); + + if ( ! isset($nonemptyComponents[$dqlAlias]) && $value !== null) { + $nonemptyComponents[$dqlAlias] = true; + } + } + + return $rowData; + } + + /** + * 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 prepares the + * values. The resulting row has the same number of elements as before. + * + * @param array $data + * @param array $cache + * @return array The processed row. + */ + protected function _gatherScalarRowData(&$data, &$cache) + { + $rowData = array(); + + foreach ($data as $key => $value) { + // Parse each column name only once. Cache the results. + if ( ! isset($cache[$key])) { + if ($this->_isIgnoredName($key)) continue; + + // cache general information like the column name <-> field name mapping + $e = explode(Doctrine_ORM_Query_ParserRule::SQLALIAS_SEPARATOR, $key); + $columnName = array_pop($e); + $cache[$key]['dqlAlias'] = $this->_tableAliases[ + implode(Doctrine_ORM_Query_ParserRule::SQLALIAS_SEPARATOR, $e) + ]; + $classMetadata = $this->_queryComponents[$cache[$key]['dqlAlias']]['metadata']; + // check whether it's an aggregate value or a regular field + if (isset($this->_queryComponents[$cache[$key]['dqlAlias']]['agg'][$columnName])) { + $fieldName = $this->_queryComponents[$cache[$key]['dqlAlias']]['agg'][$columnName]; + $cache[$key]['isScalar'] = true; + } else { + $fieldName = $this->_lookupFieldName($classMetadata, $columnName); + $cache[$key]['isScalar'] = false; + // cache type information + $cache[$key]['type'] = $classMetadata->getTypeOfColumn($columnName); + } + $cache[$key]['fieldName'] = $fieldName; + } + + $class = $this->_queryComponents[$cache[$key]['dqlAlias']]['metadata']; + $dqlAlias = $cache[$key]['dqlAlias']; + $fieldName = $cache[$key]['fieldName']; + + if ($cache[$key]['isScalar']) { + $rowData[$dqlAlias . '_' . $fieldName] = $value; + } else { + $rowData[$dqlAlias . '_' . $fieldName] = $cache[$key]['type']->convertToPHPValue($value); + } + } + + return $rowData; + } + + /** + * Gets the custom field used for indexing for the specified DQL alias. + * + * @return string The field name of the field used for indexing or NULL + * if the component does not use any custom field indices. + */ + protected function _getCustomIndexField($alias) + { + return isset($this->_queryComponents[$alias]['map']) ? $this->_queryComponents[$alias]['map'] : null; + } + + /** + * Checks whether a name is ignored. Used during result set parsing to skip + * certain elements in the result set that do not have any meaning for the result. + * (I.e. ORACLE limit/offset emulation adds doctrine_rownum to the result set). + * + * @param string $name + * @return boolean + */ + private function _isIgnoredName($name) + { + return $name == 'doctrine_rownum'; + } + + /** + * Looks up the field name for a (lowercased) column name. + * + * This is mostly used during hydration, because we want to make the + * conversion to field names while iterating over the result set for best + * performance. By doing this at that point, we can avoid re-iterating over + * the data just to convert the column names to field names. + * + * However, when this is happening, we don't know the real + * class name to instantiate yet (the row data may target a sub-type), hence + * this method looks up the field name in the subclass mappings if it's not + * found on this class mapping. + * This lookup on subclasses is costly but happens only *once* for a column + * during hydration because the hydrator caches effectively. + * + * @return string The field name. + * @throws Doctrine::ORM::Exceptions::ClassMetadataException If the field name could + * not be found. + */ + private function _lookupFieldName($class, $lcColumnName) + { + if ($class->hasLowerColumn($lcColumnName)) { + return $class->getFieldNameForLowerColumnName($lcColumnName); + } + + foreach ($class->getSubclasses() as $subClass) { + $subClassMetadata = Doctrine_ORM_Mapping_ClassMetadataFactory::getInstance() + ->getMetadataFor($subClass); + if ($subClassMetadata->hasLowerColumn($lcColumnName)) { + return $subClassMetadata->getFieldNameForLowerColumnName($lcColumnName); + } + } + + throw new Doctrine_Exception("No field name found for column name '$lcColumnName' during hydration."); + } + + /** Needed only temporarily until the new parser is ready */ + private $_isResultMixed = false; + public function setResultMixed($bool) + { + $this->_isResultMixed = $bool; + } } diff --git a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php new file mode 100644 index 000000000..18d3ede80 --- /dev/null +++ b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php @@ -0,0 +1,208 @@ +_queryComponents); + $this->_rootAlias = key($this->_queryComponents); + $this->_rootEntityName = $this->_queryComponents[$this->_rootAlias]['metadata']->getClassName(); + $this->_isSimpleQuery = count($this->_queryComponents) <= 1; + $this->_identifierMap = array(); + $this->_resultPointers = array(); + $this->_idTemplate = array(); + $this->_resultCounter = 0; + foreach ($this->_queryComponents as $dqlAlias => $component) { + $this->_identifierMap[$dqlAlias] = array(); + $this->_resultPointers[$dqlAlias] = array(); + $this->_idTemplate[$dqlAlias] = ''; + } + } + + /** @override */ + protected function _hydrateAll($parserResult) + { + $s = microtime(true); + + $result = array(); + $cache = array(); + while ($data = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { + $this->_hydrateRow($data, $cache, $result); + } + + $e = microtime(true); + echo 'Hydration took: ' . ($e - $s) . PHP_EOL; + + return $result; + } + + /** @override */ + protected function _hydrateRow(array &$data, array &$cache, &$result) + { + // 1) Initialize + $id = $this->_idTemplate; // initialize the id-memory + $nonemptyComponents = array(); + $rowData = parent::_gatherRowData($data, $cache, $id, $nonemptyComponents); + $rootAlias = $this->_rootAlias; + + // 2) Hydrate the data of the root entity from the current row + // Check for an existing element + $index = false; + if ($this->_isSimpleQuery || ! isset($this->_identifierMap[$rootAlias][$id[$rootAlias]])) { + $element = $rowData[$rootAlias]; + if ($field = $this->_getCustomIndexField($rootAlias)) { + if ($this->_parserResult->isMixedQuery()) { + $result[] = array($element[$field] => $element); + ++$this->_resultCounter; + } else { + $result[$element[$field]] = $element; + } + } else { + if ($this->_parserResult->isMixedQuery()) { + $result[] = array($element); + ++$this->_resultCounter; + } else { + $result[] = $element; + } + } + end($result); + $this->_identifierMap[$rootAlias][$id[$rootAlias]] = key($result); + } else { + $index = $this->_identifierMap[$rootAlias][$id[$rootAlias]]; + } + $this->updateResultPointer($result, $index, $rootAlias, false); + unset($rowData[$rootAlias]); + // end of hydrate data of the root component for the current row + + // Extract scalar values. They're appended at the end. + if (isset($rowData['scalars'])) { + $scalars = $rowData['scalars']; + unset($rowData['scalars']); + } + + // 3) Now hydrate the rest of the data found in the current row, that + // belongs to other (related) entities. + foreach ($rowData as $dqlAlias => $data) { + $index = false; + $map = $this->_queryComponents[$dqlAlias]; + $parent = $map['parent']; + $relationAlias = $map['relation']->getSourceFieldName(); + $path = $parent . '.' . $dqlAlias; + + // Get a reference to the right element in the result tree. + // This element will get the associated element attached. + if ($this->_parserResult->isMixedQuery() && $parent == $rootAlias) { + $key = key(reset($this->_resultPointers)); + // TODO: Exception if $key === null ? + $baseElement =& $this->_resultPointers[$parent][$key]; + } else if (isset($this->_resultPointers[$parent])) { + $baseElement =& $this->_resultPointers[$parent]; + } else { + unset($this->_resultPointers[$dqlAlias]); // Ticket #1228 + continue; + } + + // Check the type of the relation (many or single-valued) + if ( ! $map['relation']->isOneToOne()) { + $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; + $indexIsValid = $index !== false ? isset($baseElement[$relationAlias][$index]) : false; + if ( ! $indexExists || ! $indexIsValid) { + $element = $data; + if ($field = $this->_getCustomIndexField($dqlAlias)) { + $baseElement[$relationAlias][$element[$field]] = $element; + } else { + $baseElement[$relationAlias][] = $element; + } + end($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])) { + $baseElement[$relationAlias] = $data; + } + } + + $coll =& $baseElement[$relationAlias]; + + if ($coll !== null) { + $this->updateResultPointer($coll, $index, $dqlAlias, $oneToOne); + } + } + + // Append scalar values to mixed result sets + if (isset($scalars)) { + foreach ($scalars as $name => $value) { + $result[$this->_resultCounter - 1][$name] = $value; + } + } + } + + /** + * Updates the result pointer for an Entity. The result pointers point to the + * last seen instance of each Entity type. This is used for graph construction. + * + * @param array $coll The element. + * @param boolean|integer $index Index of the element in the collection. + * @param string $dqlAlias + * @param boolean $oneToOne Whether it is a single-valued association or not. + */ + private function updateResultPointer(&$coll, $index, $dqlAlias, $oneToOne) + { + if ($coll === null) { + unset($this->_resultPointers[$dqlAlias]); // Ticket #1228 + return; + } + if ($index !== false) { + $this->_resultPointers[$dqlAlias] =& $coll[$index]; + return; + } else { + if ($coll) { + if ($oneToOne) { + $this->_resultPointers[$dqlAlias] =& $coll; + } else { + end($coll); + $this->_resultPointers[$dqlAlias] =& $coll[key($coll)]; + } + } + } + } + + /** {@inheritdoc} */ + protected function _getRowContainer() + { + return array(); + } +} + diff --git a/lib/Doctrine/ORM/Internal/Hydration/IterableResult.php b/lib/Doctrine/ORM/Internal/Hydration/IterableResult.php new file mode 100644 index 000000000..27413ad01 --- /dev/null +++ b/lib/Doctrine/ORM/Internal/Hydration/IterableResult.php @@ -0,0 +1,28 @@ +_hydrator = $hydrator; + } + + /** + * Gets the next set of results. + * + * @return array + */ + public function next() + { + return $this->_hydrator->hydrateRow(); + } +} + diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectDriver.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectDriver.php index 782958884..4b871abe6 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectDriver.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectDriver.php @@ -159,13 +159,13 @@ class Doctrine_ORM_Internal_Hydration_ObjectDriver $oid2 = spl_object_hash($entity2); $sourceProp = $targetClass->getInverseAssociationMapping($fieldName)->getSourceFieldName(); $targetClass->getReflectionProperty($sourceProp)->setValue($entity2, $entity1); - $this->_entityData[$oid2][$sourceProp] = $entity1; + //$this->_entityData[$oid2][$sourceProp] = $entity1; } } else { // for sure bidirectional, as there is no inverse side in unidirectional $mappedByProp = $relation->getMappedByFieldName(); $targetClass->getReflectionProperty($mappedByProp)->setValue($entity2, $entity1); - $this->_entityData[spl_object_hash($entity2)][$mappedByProp] = $entity1; + //$this->_entityData[spl_object_hash($entity2)][$mappedByProp] = $entity1; } } } @@ -217,7 +217,6 @@ class Doctrine_ORM_Internal_Hydration_ObjectDriver public function updateResultPointer(&$resultPointers, &$coll, $index, $dqlAlias, $oneToOne) { if ($coll === null) { - echo "HERE!"; unset($resultPointers[$dqlAlias]); // Ticket #1228 return; } diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php new file mode 100644 index 000000000..a3e2c4f19 --- /dev/null +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -0,0 +1,371 @@ +_queryComponents); + $this->_rootAlias = key($this->_queryComponents); + $this->_rootEntityName = $this->_queryComponents[$this->_rootAlias]['metadata']->getClassName(); + $this->_isSimpleQuery = count($this->_queryComponents) <= 1; + $this->_identifierMap = array(); + $this->_resultPointers = array(); + $this->_idTemplate = array(); + $this->_resultCounter = 0; + foreach ($this->_queryComponents as $dqlAlias => $component) { + $this->_identifierMap[$dqlAlias] = array(); + $this->_resultPointers[$dqlAlias] = array(); + $this->_idTemplate[$dqlAlias] = ''; + } + } + + /** @override */ + protected function _hydrateAll($parserResult) + { + $s = microtime(true); + + if ($this->_parserResult->isMixedQuery()) { + $result = array(); + } else { + $result = new Doctrine_ORM_Collection($this->_em, $this->_rootEntityName); + } + + $cache = array(); + // Process result set + while ($data = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { + $this->_hydrateRow($data, $cache, $result); + } + + // Take snapshots from all initialized collections + foreach ($this->_collections as $coll) { + $coll->_takeSnapshot(); + $coll->_setHydrationFlag(false); + $this->_uow->addManagedCollection($coll); + } + + // Clean up + $this->_collections = array(); + $this->_initializedRelations = array(); + $this->_metadataMap = array(); + + $e = microtime(true); + echo 'Hydration took: ' . ($e - $s) . PHP_EOL; + + return $result; + } + + /** + * Updates the result pointer for an Entity. The result pointers point to the + * last seen instance of each Entity type. This is used for graph construction. + * + * @param array $resultPointers The result pointers. + * @param Collection $coll The element. + * @param boolean|integer $index Index of the element in the collection. + * @param string $dqlAlias + * @param boolean $oneToOne Whether it is a single-valued association or not. + */ + private function updateResultPointer(&$coll, $index, $dqlAlias, $oneToOne) + { + if ($coll === null) { + unset($this->_resultPointers[$dqlAlias]); // Ticket #1228 + return; + } + + if ($index !== false) { + $this->_resultPointers[$dqlAlias] = $coll[$index]; + return; + } + + if ( ! is_object($coll)) { + end($coll); + $this->_resultPointers[$dqlAlias] =& $coll[key($coll)]; + } else if ($coll instanceof Doctrine_ORM_Collection) { + if (count($coll) > 0) { + $this->_resultPointers[$dqlAlias] = $coll->last(); + } + } else { + $this->_resultPointers[$dqlAlias] = $coll; + } + } + + private function getElementCollection($component) + { + $coll = new Doctrine_ORM_Collection($this->_em, $component); + $this->_collections[] = $coll; + return $coll; + } + + private function initRelatedCollection($entity, $name) + { + $oid = spl_object_hash($entity); + $classMetadata = $this->_metadataMap[$oid]; + if ( ! isset($this->_initializedRelations[$oid][$name])) { + $relation = $classMetadata->getAssociationMapping($name); + $relatedClass = $this->_em->getClassMetadata($relation->getTargetEntityName()); + $coll = $this->getElementCollection($relatedClass->getClassName()); + $coll->_setOwner($entity, $relation); + $coll->_setHydrationFlag(true); + $classMetadata->getReflectionProperty($name)->setValue($entity, $coll); + $this->_initializedRelations[$oid][$name] = true; + $this->_uow->setOriginalEntityProperty($oid, $name, $coll); + } + } + + private function isIndexKeyInUse($entity, $assocField, $indexField) + { + return $this->_metadataMap[spl_object_hash($entity)]->getReflectionProperty($assocField) + ->getValue($entity)->containsKey($indexField); + } + + private function getLastKey($coll) + { + // check needed because of mixed results. + // is_object instead of is_array because is_array is slow on large arrays. + if (is_object($coll)) { + $coll->last(); + return $coll->key(); + } else { + end($coll); + return key($coll); + } + } + + private function getElement(array $data, $className) + { + $entity = $this->_em->getUnitOfWork()->createEntity($className, $data); + $oid = spl_object_hash($entity); + $this->_metadataMap[$oid] = $this->_em->getClassMetadata($className); + return $entity; + } + + /** + * Adds an element to an indexed collection-valued property. + * + * @param $entity1 + * @param $property + * @param $entity2 + * @param $indexField + */ + private function addRelatedIndexedElement($entity1, $property, $entity2, $indexField) + { + $classMetadata1 = $this->_metadataMap[spl_object_hash($entity1)]; + $classMetadata2 = $this->_metadataMap[spl_object_hash($entity2)]; + $indexValue = $classMetadata2->getReflectionProperty($indexField)->getValue($entity2); + $classMetadata1->getReflectionProperty($property)->getValue($entity1)->set($indexValue, $entity2); + } + + /** + * Adds an element to a collection-valued property. + * + * @param $entity1 + * @param $property + * @param $entity2 + */ + private function addRelatedElement($entity1, $property, $entity2) + { + $classMetadata1 = $this->_metadataMap[spl_object_hash($entity1)]; + $classMetadata1->getReflectionProperty($property)->getValue($entity1)->add($entity2); + } + + private function isFieldSet($entity, $field) + { + return $this->_metadataMap[spl_object_hash($entity)]->getReflectionProperty($field) + ->getValue($entity) !== null; + } + + /** + * Sets a related element. + * + * @param $entity1 + * @param $property + * @param $entity2 + */ + private function setRelatedElement($entity1, $property, $entity2) + { + $oid = spl_object_hash($entity1); + $classMetadata1 = $this->_metadataMap[$oid]; + $classMetadata1->getReflectionProperty($property)->setValue($entity1, $entity2); + $this->_uow->setOriginalEntityProperty($oid, $property, $entity2); + $relation = $classMetadata1->getAssociationMapping($property); + if ($relation->isOneToOne()) { + $targetClass = $this->_em->getClassMetadata($relation->getTargetEntityName()); + if ($relation->isOwningSide()) { + // If there is an inverse mapping on the target class its bidirectional + if ($targetClass->hasInverseAssociationMapping($property)) { + $oid2 = spl_object_hash($entity2); + $sourceProp = $targetClass->getInverseAssociationMapping($fieldName)->getSourceFieldName(); + $targetClass->getReflectionProperty($sourceProp)->setValue($entity2, $entity1); + } + } else { + // for sure bidirectional, as there is no inverse side in unidirectional + $mappedByProp = $relation->getMappedByFieldName(); + $targetClass->getReflectionProperty($mappedByProp)->setValue($entity2, $entity1); + } + } + } + + /** + * Hydrates a single row. + * + * @param $data The row data. + * @param $cache The cache to use. + * @param $result The result to append to. + * @override + */ + protected function _hydrateRow(array &$data, array &$cache, &$result) + { + // 1) Initialize + $id = $this->_idTemplate; // initialize the id-memory + $nonemptyComponents = array(); + $rowData = parent::_gatherRowData($data, $cache, $id, $nonemptyComponents); + $rootAlias = $this->_rootAlias; + + // 2) Hydrate the data of the root entity from the current row + // Check for an existing element + $index = false; + if ($this->_isSimpleQuery || ! isset($this->_identifierMap[$rootAlias][$id[$rootAlias]])) { + $element = $this->_uow->createEntity($this->_rootEntityName, $rowData[$rootAlias]); + $oid = spl_object_hash($element); + $this->_metadataMap[$oid] = $this->_em->getClassMetadata($this->_rootEntityName); + if ($field = $this->_getCustomIndexField($rootAlias)) { + if ($this->_parserResult->isMixedQuery()) { + $result[] = array( + $this->_metadataMap[$oid]->getReflectionProperty($field) + ->getValue($element) => $element + ); + ++$this->_resultCounter; + } else { + $result->set($element, $this->_metadataMap[$oid] + ->getReflectionProperty($field) + ->getValue($element)); + } + } else { + if ($this->_parserResult->isMixedQuery()) { + $result[] = array($element); + ++$this->_resultCounter; + } else { + $result->add($element); + } + } + $this->_identifierMap[$rootAlias][$id[$rootAlias]] = $this->getLastKey($result); + } else { + $index = $this->_identifierMap[$rootAlias][$id[$rootAlias]]; + } + $this->updateResultPointer($result, $index, $rootAlias, false); + unset($rowData[$rootAlias]); + // end hydrate data of the root component for the current row + + // Extract scalar values. They're appended at the end. + if (isset($rowData['scalars'])) { + $scalars = $rowData['scalars']; + unset($rowData['scalars']); + } + + // 3) Now hydrate the rest of the data found in the current row, that + // belongs to other (related) entities. + foreach ($rowData as $dqlAlias => $data) { + $index = false; + $map = $this->_queryComponents[$dqlAlias]; + $entityName = $map['metadata']->getClassName(); + $parent = $map['parent']; + $relationAlias = $map['relation']->getSourceFieldName(); + $path = $parent . '.' . $dqlAlias; + + // Get a reference to the right element in the result tree. + // This element will get the associated element attached. + if ($this->_parserResult->isMixedQuery() && $parent == $rootAlias) { + $key = key(reset($this->_resultPointers)); + // TODO: Exception if $key === null ? + $baseElement =& $this->_resultPointers[$parent][$key]; + } else if (isset($this->_resultPointers[$parent])) { + $baseElement =& $this->_resultPointers[$parent]; + } else { + unset($this->_resultPointers[$dqlAlias]); // Ticket #1228 + continue; + } + + $oid = spl_object_hash($baseElement); + + // Check the type of the relation (many or single-valued) + if ( ! $map['relation']->isOneToOne()) { + $oneToOne = false; + if (isset($nonemptyComponents[$dqlAlias])) { + $this->initRelatedCollection($baseElement, $relationAlias); + $indexExists = isset($this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]]); + $index = $indexExists ? $this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false; + $indexIsValid = $index !== false ? $this->isIndexKeyInUse($baseElement, $relationAlias, $index) : false; + if ( ! $indexExists || ! $indexIsValid) { + $element = $this->getElement($data, $entityName); + if ($field = $this->_getCustomIndexField($dqlAlias)) { + $this->addRelatedIndexedElement($baseElement, $relationAlias, $element, $field); + } else { + $this->addRelatedElement($baseElement, $relationAlias, $element); + } + $this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = $this->getLastKey( + $this->_metadataMap[$oid] + ->getReflectionProperty($relationAlias) + ->getValue($baseElement)); + } + } else if ( ! $this->isFieldSet($baseElement, $relationAlias)) { + $coll = new Doctrine_ORM_Collection($this->_em, $entityName); + $this->_collections[] = $coll; + $this->setRelatedElement($baseElement, $relationAlias, $coll); + } + } else { + $oneToOne = true; + if ( ! isset($nonemptyComponents[$dqlAlias]) && + ! $this->isFieldSet($baseElement, $relationAlias)) { + $this->setRelatedElement($baseElement, $relationAlias, null); + } else if ( ! $this->isFieldSet($baseElement, $relationAlias)) { + $this->setRelatedElement($baseElement, $relationAlias, + $this->getElement($data, $entityName)); + } + } + + $coll = $this->_metadataMap[$oid] + ->getReflectionProperty($relationAlias) + ->getValue($baseElement); + + if ($coll !== null) { + $this->updateResultPointer($coll, $index, $dqlAlias, $oneToOne); + } + } + + // Append scalar values to mixed result sets + if (isset($scalars)) { + foreach ($scalars as $name => $value) { + $result[$this->_resultCounter - 1][$name] = $value; + } + } + } + + /** {@inheritdoc} */ + protected function _getRowContainer() + { + return new Doctrine_Common_Collections_Collection; + } +} + diff --git a/lib/Doctrine/ORM/Internal/Hydration/ScalarHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ScalarHydrator.php new file mode 100644 index 000000000..6966604f2 --- /dev/null +++ b/lib/Doctrine/ORM/Internal/Hydration/ScalarHydrator.php @@ -0,0 +1,36 @@ +_stmt->fetch(PDO::FETCH_ASSOC)) { + $result[] = $this->_gatherScalarRowData($data, $cache); + } + return $result; + } + + /** @override */ + protected function _hydrateRow(array &$data, array &$cache, &$result) + { + $result[] = $this->_gatherScalarRowData($data, $cache); + } + + /** @override */ + protected function _getRowContainer() + { + return array(); + } +} + diff --git a/lib/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php new file mode 100644 index 000000000..72870fb1e --- /dev/null +++ b/lib/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php @@ -0,0 +1,33 @@ +_stmt->fetchAll(PDO::FETCH_ASSOC); + //TODO: Let this exception be raised by Query as QueryException + if (count($result) > 1 || count($result[0]) > 1) { + throw Doctrine_ORM_Exceptions_HydrationException::nonUniqueResult(); + } + $result = $this->_gatherScalarRowData($result[0], $cache); + return array_shift($result); + } + + /** {@inheritdoc} */ + protected function _getRowContainer() + { + return array(); + } +} + diff --git a/lib/Doctrine/ORM/Internal/Hydration/StandardHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/StandardHydrator.php index d81b19bef..4192fce72 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/StandardHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/StandardHydrator.php @@ -50,7 +50,7 @@ * for the "numRowsInResult * numColumnsInResult" part is crucial to fast hydration. * * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org + * @link www.doctrine-project.org * @since 1.0 * @version $Revision: 3192 $ * @author Konsta Vesterinen @@ -290,266 +290,4 @@ class Doctrine_ORM_Internal_Hydration_StandardHydrator extends Doctrine_ORM_Inte return $result; } - /** - * Processes a row of the result set. - * Used for identity hydration (HYDRATE_IDENTITY_OBJECT and HYDRATE_IDENTITY_ARRAY). - * Puts the elements of a result row into a new array, grouped by the class - * they belong to. The column names in the result set are mapped to their - * field names during this procedure as well as any necessary conversions on - * the values applied. - * - * @return array An array with all the fields (name => value) of the data row, - * grouped by their component (alias). - * @todo Significant code duplication with _gatherScalarRowData(). Good refactoring - * possible without sacrificing performance? - */ - protected function _gatherRowData(&$data, &$cache, &$id, &$nonemptyComponents) - { - $rowData = array(); - - foreach ($data as $key => $value) { - // Parse each column name only once. Cache the results. - if ( ! isset($cache[$key])) { - if ($this->_isIgnoredName($key)) continue; - - // Cache general information like the column name <-> field name mapping - $e = explode(Doctrine_ORM_Query_ParserRule::SQLALIAS_SEPARATOR, $key); - $columnName = array_pop($e); - $cache[$key]['dqlAlias'] = $this->_tableAliases[ - implode(Doctrine_ORM_Query_ParserRule::SQLALIAS_SEPARATOR, $e) - ]; - $classMetadata = $this->_queryComponents[$cache[$key]['dqlAlias']]['metadata']; - // check whether it's an aggregate value or a regular field - if (isset($this->_queryComponents[$cache[$key]['dqlAlias']]['agg'][$columnName])) { - $fieldName = $this->_queryComponents[$cache[$key]['dqlAlias']]['agg'][$columnName]; - $cache[$key]['isScalar'] = true; - } else { - $fieldName = $this->_lookupFieldName($classMetadata, $columnName); - $cache[$key]['isScalar'] = false; - // cache type information - $cache[$key]['type'] = $classMetadata->getTypeOfColumn($columnName); - } - - $cache[$key]['fieldName'] = $fieldName; - - // cache identifier information - if ($classMetadata->isIdentifier($fieldName)) { - $cache[$key]['isIdentifier'] = true; - } else { - $cache[$key]['isIdentifier'] = false; - } - } - - $class = $this->_queryComponents[$cache[$key]['dqlAlias']]['metadata']; - $dqlAlias = $cache[$key]['dqlAlias']; - $fieldName = $cache[$key]['fieldName']; - - if ($cache[$key]['isScalar']) { - $rowData['scalars'][$fieldName] = $value; - continue; - } - - if ($cache[$key]['isIdentifier']) { - $id[$dqlAlias] .= '|' . $value; - } - - if ($cache[$key]['isScalar']) { - $rowData[$dqlAlias][$fieldName] = $value; - } else { - $rowData[$dqlAlias][$fieldName] = $cache[$key]['type']->convertToPHPValue($value); - } - - if ( ! isset($nonemptyComponents[$dqlAlias]) && $value !== null) { - $nonemptyComponents[$dqlAlias] = true; - } - } - - return $rowData; - } - - /** - * 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 prepares the - * values. The resulting row has the same number of elements as before. - * - * @param array $data - * @param array $cache - * @return array The processed row. - * @todo Significant code duplication with _gatherRowData(). Good refactoring - * possible without sacrificing performance? - */ - private function _gatherScalarRowData(&$data, &$cache) - { - $rowData = array(); - - foreach ($data as $key => $value) { - // Parse each column name only once. Cache the results. - if ( ! isset($cache[$key])) { - if ($this->_isIgnoredName($key)) continue; - - // cache general information like the column name <-> field name mapping - $e = explode(Doctrine_ORM_Query_ParserRule::SQLALIAS_SEPARATOR, $key); - $columnName = array_pop($e); - $cache[$key]['dqlAlias'] = $this->_tableAliases[ - implode(Doctrine_ORM_Query_ParserRule::SQLALIAS_SEPARATOR, $e) - ]; - $classMetadata = $this->_queryComponents[$cache[$key]['dqlAlias']]['metadata']; - // check whether it's an aggregate value or a regular field - if (isset($this->_queryComponents[$cache[$key]['dqlAlias']]['agg'][$columnName])) { - $fieldName = $this->_queryComponents[$cache[$key]['dqlAlias']]['agg'][$columnName]; - $cache[$key]['isScalar'] = true; - } else { - $fieldName = $this->_lookupFieldName($classMetadata, $columnName); - $cache[$key]['isScalar'] = false; - // cache type information - $cache[$key]['type'] = $classMetadata->getTypeOfColumn($columnName); - } - $cache[$key]['fieldName'] = $fieldName; - } - - $class = $this->_queryComponents[$cache[$key]['dqlAlias']]['metadata']; - $dqlAlias = $cache[$key]['dqlAlias']; - $fieldName = $cache[$key]['fieldName']; - - if ($cache[$key]['isScalar']) { - $rowData[$dqlAlias . '_' . $fieldName] = $value; - } else { - $rowData[$dqlAlias . '_' . $fieldName] = $cache[$key]['type']->convertToPHPValue($value); - } - } - - return $rowData; - } - - /** - * Gets the custom field used for indexing for the specified component alias. - * - * @return string The field name of the field used for indexing or NULL - * if the component does not use any custom field indices. - */ - private function _getCustomIndexField($alias) - { - return isset($this->_queryComponents[$alias]['map']) ? $this->_queryComponents[$alias]['map'] : null; - } - - /** - * Checks whether a name is ignored. Used during result set parsing to skip - * certain elements in the result set that do not have any meaning for the result. - * (I.e. ORACLE limit/offset emulation adds doctrine_rownum to the result set). - * - * @param string $name - * @return boolean - */ - private function _isIgnoredName($name) - { - return $name == 'doctrine_rownum'; - } - - /** - * Looks up the field name for a (lowercased) column name. - * - * This is mostly used during hydration, because we want to make the - * conversion to field names while iterating over the result set for best - * performance. By doing this at that point, we can avoid re-iterating over - * the data just to convert the column names to field names. - * - * However, when this is happening, we don't know the real - * class name to instantiate yet (the row data may target a sub-type), hence - * this method looks up the field name in the subclass mappings if it's not - * found on this class mapping. - * This lookup on subclasses is costly but happens only *once* for a column - * during hydration because the hydrator caches effectively. - * - * @return string The field name. - * @throws Doctrine::ORM::Exceptions::ClassMetadataException If the field name could - * not be found. - */ - private function _lookupFieldName($class, $lcColumnName) - { - if ($class->hasLowerColumn($lcColumnName)) { - return $class->getFieldNameForLowerColumnName($lcColumnName); - } - - foreach ($class->getSubclasses() as $subClass) { - $subClassMetadata = Doctrine_ORM_Mapping_ClassMetadataFactory::getInstance() - ->getMetadataFor($subClass); - if ($subClassMetadata->hasLowerColumn($lcColumnName)) { - return $subClassMetadata->getFieldNameForLowerColumnName($lcColumnName); - } - } - - throw new Doctrine_Exception("No field name found for column name '$lcColumnName' during hydration."); - } - - /** - * this method performs special data preparation depending on - * the type of the given column - * - * 1. It unserializes array and object typed columns - * 2. Uncompresses gzip typed columns - * 3. Gets the appropriate enum values for enum typed columns - * 4. Initializes special null object pointer for null values (for fast column existence checking purposes) - * - * example: - * - * $field = 'name'; - * $value = null; - * $table->prepareValue($field, $value); // Doctrine_Null - * - * - * @param string $field the name of the field - * @param string $value field value - * @param string $typeHint A hint on the type of the value. If provided, the type lookup - * for the field can be skipped. Used i.e. during hydration to - * improve performance on large and/or complex results. - * @return mixed prepared value - * @todo Remove. Should be handled by the Type classes. No need for this switch stuff. - */ - public function prepareValue(Doctrine_ORM_Mapping_ClassMetadata $class, $fieldName, $value, $typeHint = null) - { - if ($value === $this->_nullObject) { - return $this->_nullObject; - } else if ($value === null) { - return null; - } else { - $type = is_null($typeHint) ? $class->getTypeOf($fieldName) : $typeHint; - switch ($type) { - case 'integer': - case 'string': - case 'enum': - case 'boolean': - // don't do any conversions on primitive types - break; - case 'array': - case 'object': - if (is_string($value)) { - $value = unserialize($value); - if ($value === false) { - throw new Doctrine_Hydrator_Exception('Unserialization of ' . $fieldName . ' failed.'); - } - return $value; - } - break; - case 'gzip': - $value = gzuncompress($value); - if ($value === false) { - throw new Doctrine_Hydrator_Exception('Uncompressing of ' . $fieldName . ' failed.'); - } - return $value; - break; - } - } - return $value; - } - - - - /** Needed only temporarily until the new parser is ready */ - private $_isResultMixed = false; - public function setResultMixed($bool) - { - $this->_isResultMixed = $bool; - } - } diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index 6cbd0896b..28de235e2 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -65,9 +65,9 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract protected $_entityManager; /** - * @var Doctrine\ORM\Internal\Hydrator The hydrator object used to hydrate query results. + * @var integer The hydration mode. */ - protected $_hydrator; + protected $_hydrationMode = self::HYDRATE_OBJECT; /** * @var Doctrine\ORM\Query\ParserResult The parser result that holds DQL => SQL information. @@ -125,12 +125,11 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract public function __construct(Doctrine_ORM_EntityManager $entityManager) { $this->_entityManager = $entityManager; - $this->_hydrator = new Doctrine_ORM_Internal_Hydration_StandardHydrator($entityManager); $this->free(); } /** - * Retrieves the assocated EntityManager to this Doctrine_ORM_Query + * Retrieves the assocated EntityManager to this Query instance. * * @return Doctrine\ORM\EntityManager */ @@ -139,23 +138,14 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract return $this->_entityManager; } - /** - * Returns the hydrator associated with this query object - * - * @return Doctrine\ORM\Internal\StandardHydrator The hydrator associated with this query object - */ - public function getHydrator() - { - return $this->_hydrator; - } - /** * Convenience method to execute using array fetching as hydration mode. * * @param string $params * @return array */ - public function fetchArray($params = array()) { + public function fetchArray($params = array()) + { return $this->execute($params, self::HYDRATE_ARRAY); } @@ -187,9 +177,9 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract /** * Query the database with DQL (Doctrine Query Language). * - * @param string $query DQL query - * @param array $params prepared statement parameters - * @param int $hydrationMode Doctrine::FETCH_ARRAY or Doctrine::FETCH_RECORD + * @param string $query The DQL query. + * @param array $params The query parameters. + * @param int $hydrationMode * @return mixed */ public function query($query, $params = array(), $hydrationMode = null) @@ -199,8 +189,7 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract } /** - * Builds the sql query from the given parameters and applies things such as - * column aggregation inheritance and limit subqueries if needed + * Gets the SQL query/queries that correspond to this DQL query. * * @return mixed The built sql query or an array of all sql queries. */ @@ -211,8 +200,10 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract /** * Parses the DQL query, if necessary, and stores the parser result. + * + * Note: Populates $this->_parserResult as a side-effect. * - * @return Doctrine_ORM_Query_ParserResult + * @return Doctrine\ORM\Query\ParserResult */ public function parse() { @@ -235,9 +226,13 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract */ public function execute($params = array(), $hydrationMode = null) { + if ($hydrationMode !== null) { + $this->_hydrationMode = $hydrationMode; + } + $params = $this->getParams($params); - // If there is a CacheDriver associated to cache resultsets... + // Check result cache if ($this->_resultCache && $this->_type === self::SELECT) { // Only executes if "SELECT" $cacheDriver = $this->getResultCacheDriver(); @@ -259,42 +254,23 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract return $queryResult->getResultSet(); } } - - return $this->_execute($params, $hydrationMode); - } - - /** - * _execute - * - * @param string $params Parameters to be sent to query. - * @param int $hydrationMode Method of hydration to be used. - * @return Doctrine_Collection The root collection - */ - protected function _execute($params, $hydrationMode) - { - // preQuery invoking - $this->preQuery(); - - // Query execution - $stmt = $this->_execute2($params); - - // postQuery invoking - $this->postQuery(); + + $stmt = $this->_execute($params); if (is_integer($stmt)) { return $stmt; } - return $this->_hydrator->hydrateResultSet($stmt, $hydrationMode); + return $this->_em->getHydrator($this->_hydrationMode)->hydrateAll($stmt, $this->_parserResult); } /** - * _execute2 + * _execute * * @param array $params * @return PDOStatement The executed PDOStatement. */ - protected function _execute2($params) + protected function _execute(array $params) { // If there is a CacheDriver associated to cache queries... if ($this->_queryCache || $this->_entityManager->getConnection()->getAttribute(Doctrine::ATTR_QUERY_CACHE)) { @@ -320,9 +296,7 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract $executor = $this->parse()->getSqlExecutor(); } - // Assignments for Hydrator and Enums - $this->_hydrator->setQueryComponents($this->_parserResult->getQueryComponents()); - $this->_hydrator->setTableAliasMap($this->_parserResult->getTableAliasMap()); + // Assignments for Enums $this->_setEnumParams($this->_parserResult->getEnumParams()); // Converting parameters @@ -335,7 +309,7 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract $params = array_merge($params, $params); } - // Executing the query and assigning PDOStatement + // Executing the query and returning statement return $executor->execute($this->_conn, $params); } @@ -354,17 +328,16 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract /** * Defines a cache driver to be used for caching result sets. * - * @param Doctrine_Cache_Interface|null $driver Cache driver - * @return Doctrine_ORM_Query + * @param Doctrine\ORM\Cache\Cache $driver Cache driver + * @return Doctrine\ORM\Query */ public function setResultCache($resultCache) { - if ($resultCache !== null && ! ($resultCache instanceof Doctrine_Cache_Interface)) { + if ($resultCache !== null && ! ($resultCache instanceof Doctrine_ORM_Cache_Cache)) { throw new Doctrine_ORM_Query_Exception( 'Method setResultCache() accepts only an instance of Doctrine_Cache_Interface or null.' ); } - $this->_resultCache = $resultCache; return $this; @@ -377,7 +350,7 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract */ public function getResultCache() { - if ($this->_resultCache instanceof Doctrine_ORM_Cache_Interface) { + if ($this->_resultCache instanceof Doctrine_ORM_Cache_Cache) { return $this->_resultCache; } else { return $this->_entityManager->getConnection()->getResultCacheDriver(); @@ -388,7 +361,7 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract * Defines how long the result cache will be active before expire. * * @param integer $timeToLive How long the cache entry is valid - * @return Doctrine_ORM_Query + * @return Doctrine\ORM\Query */ public function setResultCacheLifetime($timeToLive) { @@ -442,7 +415,7 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract */ public function setQueryCache($queryCache) { - if ($queryCache !== null && ! ($queryCache instanceof Doctrine_ORM_Cache_Interface)) { + if ($queryCache !== null && ! ($queryCache instanceof Doctrine_ORM_Cache_Cache)) { throw new Doctrine_ORM_Query_Exception( 'Method setResultCache() accepts only an instance of Doctrine_ORM_Cache_Interface or null.' ); @@ -460,7 +433,7 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract */ public function getQueryCache() { - if ($this->_queryCache instanceof Doctrine_ORM_Cache_Interface) { + if ($this->_queryCache instanceof Doctrine_ORM_Cache_Cache) { return $this->_queryCache; } else { return $this->_entityManager->getConnection()->getQueryCacheDriver(); @@ -522,46 +495,19 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract * * @param integer $hydrationMode Doctrine processing mode to be used during hydration process. * One of the Doctrine::HYDRATE_* constants. - * @return Doctrine_ORM_Query + * @return Doctrine\ORM\Query */ public function setHydrationMode($hydrationMode) { - $this->_hydrator->setHydrationMode($hydrationMode); - + $this->_hydrationMode = $hydrationMode; return $this; } - - /** - * Empty template method to provide Query subclasses with the possibility - * to hook into the query building procedure, doing any custom / specialized - * query building procedures that are neccessary. - * - * @return void - * @deprecated Should be removed. Extending Query is no good solution. Should - * Things like this should be done through listeners. - */ - public function preQuery() - { - - } - - /** - * Empty template method to provide Query subclasses with the possibility - * to hook into the query building procedure, doing any custom / specialized - * post query procedures (for example logging) that are neccessary. - * - * @return void - * @deprecated Should be removed. Extending Query is no good solution. Should - * Things like this should be done through listeners. - */ - public function postQuery() - { - - } /** * Gets the list of results for the query. * + * Alias for execute(array(), $hydrationMode). + * * @param integer $hydrationMode * @return mixed */ @@ -613,23 +559,15 @@ class Doctrine_ORM_Query extends Doctrine_ORM_Query_Abstract } /** - * This method is automatically called when this Doctrine_Hydrate is serialized. + * Executes the query and returns an IterableResult that can be iterated over. + * Objects in the result are initialized on-demand. * - * @return array An array of serialized properties + * @return IterableResult */ - public function serialize() + public function iterate(array $params = array(), $hydrationMode = self::HYDRATE_OBJECT) { - $vars = get_object_vars($this); - } - - /** - * This method is automatically called everytime a Doctrine_Hydrate object is unserialized. - * - * @param string $serialized Doctrine_Record as serialized string - * @return void - */ - public function unserialize($serialized) - { - + return $this->_em->getHydrator($this->_hydrationMode)->iterate( + $this->_execute($params, $hydrationMode), $this->_parserResult + ); } } diff --git a/lib/Doctrine/ORM/Query/ParserResultDummy.php b/lib/Doctrine/ORM/Query/ParserResultDummy.php index 2dbfa61eb..896af3c7e 100644 --- a/lib/Doctrine/ORM/Query/ParserResultDummy.php +++ b/lib/Doctrine/ORM/Query/ParserResultDummy.php @@ -54,6 +54,11 @@ class Doctrine_ORM_Query_ParserResultDummy { return $this->_tableToClassAliasMap; } + + public function getTableAliasMap() + { + return $this->_tableToClassAliasMap; + } public function setTableToClassAliasMap(array $map) { diff --git a/lib/Doctrine/ORM/VirtualProxy.php b/lib/Doctrine/ORM/VirtualProxy.php index 753355c61..87732dcbb 100644 --- a/lib/Doctrine/ORM/VirtualProxy.php +++ b/lib/Doctrine/ORM/VirtualProxy.php @@ -68,4 +68,4 @@ class Doctrine_ORM_VirtualProxy unset($realInstance->$prop); } } -?> + diff --git a/tests/Orm/Hydration/AllTests.php b/tests/Orm/Hydration/AllTests.php index ccca36b7e..9cfc88116 100644 --- a/tests/Orm/Hydration/AllTests.php +++ b/tests/Orm/Hydration/AllTests.php @@ -6,7 +6,11 @@ if (!defined('PHPUnit_MAIN_METHOD')) { require_once 'lib/DoctrineTestInit.php'; // Tests -require_once 'Orm/Hydration/BasicHydrationTest.php'; +//require_once 'Orm/Hydration/BasicHydrationTest.php'; +require_once 'Orm/Hydration/ObjectHydratorTest.php'; +require_once 'Orm/Hydration/ArrayHydratorTest.php'; +require_once 'Orm/Hydration/ScalarHydratorTest.php'; +require_once 'Orm/Hydration/SingleScalarHydratorTest.php'; class Orm_Hydration_AllTests { @@ -19,7 +23,11 @@ class Orm_Hydration_AllTests { $suite = new Doctrine_TestSuite('Doctrine Orm Hydration'); - $suite->addTestSuite('Orm_Hydration_BasicHydrationTest'); + //$suite->addTestSuite('Orm_Hydration_BasicHydrationTest'); + $suite->addTestSuite('Orm_Hydration_ObjectHydratorTest'); + $suite->addTestSuite('Orm_Hydration_ArrayHydratorTest'); + $suite->addTestSuite('Orm_Hydration_ScalarHydratorTest'); + $suite->addTestSuite('Orm_Hydration_SingleScalarHydratorTest'); return $suite; } diff --git a/tests/Orm/Hydration/ArrayHydratorTest.php b/tests/Orm/Hydration/ArrayHydratorTest.php new file mode 100644 index 000000000..5bdf952b2 --- /dev/null +++ b/tests/Orm/Hydration/ArrayHydratorTest.php @@ -0,0 +1,712 @@ + array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'map' => null + ) + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u' + ); + + // Faked result set + $resultSet = array( + array( + 'u__id' => '1', + 'u__name' => 'romanb' + ), + array( + 'u__id' => '2', + 'u__name' => 'jwage' + ) + ); + + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ArrayHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_ARRAY)); + + $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 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 + */ + public function testNewHydrationMixedQueryFetchJoin() + { + // Faked query components + $queryComponents = array( + 'u' => array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'map' => null, + 'agg' => array('0' => 'nameUpper') + ), + 'p' => array( + 'metadata' => $this->_em->getClassMetadata('CmsPhonenumber'), + 'parent' => 'u', + 'relation' => $this->_em->getClassMetadata('CmsUser')->getAssociationMapping('phonenumbers'), + 'map' => null + ) + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u', + 'p' => 'p' + ); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '42', + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '43', + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'u__0' => 'JWAGE', + 'p__phonenumber' => '91' + ) + ); + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ArrayHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_ARRAY, true)); + + $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('ROMANB', $result[0]['nameUpper']); + // second user => 1 phonenumber + $this->assertEquals(1, count($result[1][0]['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']); + } + + /** + * 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 + */ + public function testNewHydrationMixedQueryNormalJoin() + { + // Faked query components + $queryComponents = array( + 'u' => array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'map' => null + ), + 'p' => array( + 'metadata' => $this->_em->getClassMetadata('CmsPhonenumber'), + 'parent' => 'u', + 'relation' => $this->_em->getClassMetadata('CmsUser')->getAssociationMapping('phonenumbers'), + 'map' => null, + 'agg' => array('0' => 'numPhones') + ) + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u', + 'p' => 'p' + ); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'p__0' => '2', + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'p__0' => '1', + ) + ); + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ArrayHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_ARRAY, true)); + + $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, $result[0]['numPhones']); + // second user => 1 phonenumber + $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 + */ + public function testNewHydrationMixedQueryFetchJoinCustomIndex() + { + // Faked query components + $queryComponents = array( + 'u' => array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'agg' => array('0' => 'nameUpper'), + 'map' => 'id' + ), + 'p' => array( + 'metadata' => $this->_em->getClassMetadata('CmsPhonenumber'), + 'parent' => 'u', + 'relation' => $this->_em->getClassMetadata('CmsUser')->getAssociationMapping('phonenumbers'), + 'map' => 'phonenumber' + ) + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u', + 'p' => 'p' + ); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '42', + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '43', + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'u__0' => 'JWAGE', + 'p__phonenumber' => '91' + ) + ); + + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ArrayHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_ARRAY, true)); + + $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); + $this->assertTrue(is_array($result[0])); + $this->assertTrue(is_array($result[1])); + + // test the scalar values + $this->assertEquals('ROMANB', $result[0]['nameUpper']); + $this->assertEquals('JWAGE', $result[1]['nameUpper']); + // first user => 2 phonenumbers. notice the custom indexing by user id + $this->assertEquals(2, count($result[0]['1']['phonenumbers'])); + // second user => 1 phonenumber. notice the custom indexing by user id + $this->assertEquals(1, count($result[1]['2']['phonenumbers'])); + // test the custom indexing of the phonenumbers + $this->assertTrue(isset($result[0]['1']['phonenumbers']['42'])); + $this->assertTrue(isset($result[0]['1']['phonenumbers']['43'])); + $this->assertTrue(isset($result[1]['2']['phonenumbers']['91'])); + } + + /** + * select u.id, u.status, p.phonenumber, upper(u.name) nameUpper, a.id, a.topic + * from User u + * join u.phonenumbers p + * join u.articles a + * = + * select u.id, u.status, p.phonenumber, upper(u.name) as u__0, a.id, a.topic + * from USERS u + * inner join PHONENUMBERS p ON u.id = p.user_id + * inner join ARTICLES a ON u.id = a.user_id + */ + public function testNewHydrationMixedQueryMultipleFetchJoin() + { + // Faked query components + $queryComponents = array( + 'u' => array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'map' => null, + 'agg' => array('0' => 'nameUpper') + ), + 'p' => array( + 'metadata' => $this->_em->getClassMetadata('CmsPhonenumber'), + 'parent' => 'u', + 'relation' => $this->_em->getClassMetadata('CmsUser')->getAssociationMapping('phonenumbers'), + 'map' => null + ), + 'a' => array( + 'metadata' => $this->_em->getClassMetadata('CmsArticle'), + 'parent' => 'u', + 'relation' => $this->_em->getClassMetadata('CmsUser')->getAssociationMapping('articles'), + 'map' => null + ), + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u', + 'p' => 'p', + 'a' => 'a' + ); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '42', + 'a__id' => '1', + 'a__topic' => 'Getting things done!' + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '43', + 'a__id' => '1', + 'a__topic' => 'Getting things done!' + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '42', + 'a__id' => '2', + 'a__topic' => 'ZendCon' + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '43', + 'a__id' => '2', + 'a__topic' => 'ZendCon' + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'u__0' => 'JWAGE', + 'p__phonenumber' => '91', + 'a__id' => '3', + 'a__topic' => 'LINQ' + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'u__0' => 'JWAGE', + 'p__phonenumber' => '91', + 'a__id' => '4', + 'a__topic' => 'PHP6' + ), + ); + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ArrayHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_ARRAY, true)); + + $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, 2 articles + $this->assertEquals(2, count($result[0][0]['phonenumbers'])); + $this->assertEquals(2, count($result[0][0]['articles'])); + $this->assertEquals('ROMANB', $result[0]['nameUpper']); + // second user => 1 phonenumber, 2 articles + $this->assertEquals(1, count($result[1][0]['phonenumbers'])); + $this->assertEquals(2, count($result[1][0]['articles'])); + $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('Getting things done!', $result[0][0]['articles'][0]['topic']); + $this->assertEquals('ZendCon', $result[0][0]['articles'][1]['topic']); + $this->assertEquals('LINQ', $result[1][0]['articles'][0]['topic']); + $this->assertEquals('PHP6', $result[1][0]['articles'][1]['topic']); + } + + /** + * select u.id, u.status, p.phonenumber, upper(u.name) nameUpper, a.id, a.topic, + * c.id, c.topic + * from User u + * join u.phonenumbers p + * join u.articles a + * left join a.comments c + * = + * select u.id, u.status, p.phonenumber, upper(u.name) as u__0, a.id, a.topic, + * c.id, c.topic + * from USERS u + * inner join PHONENUMBERS p ON u.id = p.user_id + * inner join ARTICLES a ON u.id = a.user_id + * left outer join COMMENTS c ON a.id = c.article_id + */ + public function testNewHydrationMixedQueryMultipleDeepMixedFetchJoin() + { + // Faked query components + $queryComponents = array( + 'u' => array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'map' => null, + 'agg' => array('0' => 'nameUpper') + ), + 'p' => array( + 'metadata' => $this->_em->getClassMetadata('CmsPhonenumber'), + 'parent' => 'u', + 'relation' => $this->_em->getClassMetadata('CmsUser')->getAssociationMapping('phonenumbers'), + 'map' => null + ), + 'a' => array( + 'metadata' => $this->_em->getClassMetadata('CmsArticle'), + 'parent' => 'u', + 'relation' => $this->_em->getClassMetadata('CmsUser')->getAssociationMapping('articles'), + 'map' => null + ), + 'c' => array( + 'metadata' => $this->_em->getClassMetadata('CmsComment'), + 'parent' => 'a', + 'relation' => $this->_em->getClassMetadata('CmsArticle')->getAssociationMapping('comments'), + 'map' => null + ), + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u', + 'p' => 'p', + 'a' => 'a', + 'c' => 'c' + ); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '42', + 'a__id' => '1', + 'a__topic' => 'Getting things done!', + 'c__id' => '1', + 'c__topic' => 'First!' + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '43', + 'a__id' => '1', + 'a__topic' => 'Getting things done!', + 'c__id' => '1', + 'c__topic' => 'First!' + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '42', + 'a__id' => '2', + 'a__topic' => 'ZendCon', + 'c__id' => null, + 'c__topic' => null + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '43', + 'a__id' => '2', + 'a__topic' => 'ZendCon', + 'c__id' => null, + 'c__topic' => null + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'u__0' => 'JWAGE', + 'p__phonenumber' => '91', + 'a__id' => '3', + 'a__topic' => 'LINQ', + 'c__id' => null, + 'c__topic' => null + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'u__0' => 'JWAGE', + 'p__phonenumber' => '91', + 'a__id' => '4', + 'a__topic' => 'PHP6', + 'c__id' => null, + 'c__topic' => null + ), + ); + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ArrayHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_ARRAY, true)); + + $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, 2 articles, 1 comment on first article + $this->assertEquals(2, count($result[0][0]['phonenumbers'])); + $this->assertEquals(2, count($result[0][0]['articles'])); + $this->assertEquals(1, count($result[0][0]['articles'][0]['comments'])); + $this->assertEquals('ROMANB', $result[0]['nameUpper']); + // second user => 1 phonenumber, 2 articles, no comments + $this->assertEquals(1, count($result[1][0]['phonenumbers'])); + $this->assertEquals(2, count($result[1][0]['articles'])); + $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('Getting things done!', $result[0][0]['articles'][0]['topic']); + $this->assertEquals('ZendCon', $result[0][0]['articles'][1]['topic']); + $this->assertEquals('LINQ', $result[1][0]['articles'][0]['topic']); + $this->assertEquals('PHP6', $result[1][0]['articles'][1]['topic']); + + $this->assertEquals('First!', $result[0][0]['articles'][0]['comments'][0]['topic']); + + $this->assertTrue(isset($result[0][0]['articles'][0]['comments'])); + + // empty comment collections + $this->assertTrue(is_array($result[0][0]['articles'][1]['comments'])); + $this->assertEquals(0, count($result[0][0]['articles'][1]['comments'])); + $this->assertTrue(is_array($result[1][0]['articles'][0]['comments'])); + $this->assertEquals(0, count($result[1][0]['articles'][0]['comments'])); + $this->assertTrue(is_array($result[1][0]['articles'][1]['comments'])); + $this->assertEquals(0, count($result[1][0]['articles'][1]['comments'])); + } + + /** + * Tests that the hydrator does not rely on a particular order of the rows + * in the result set. + * + * DQL: + * select c.id, c.position, c.name, b.id, b.position + * from ForumCategory c inner join c.boards b + * order by c.position asc, b.position asc + * + * Checks whether the boards are correctly assigned to the categories. + * + * The 'evil' result set that confuses the object population is displayed below. + * + * c.id | c.position | c.name | boardPos | b.id | b.category_id (just for clarity) + * 1 | 0 | First | 0 | 1 | 1 + * 2 | 0 | Second | 0 | 2 | 2 <-- + * 1 | 0 | First | 1 | 3 | 1 + * 1 | 0 | First | 2 | 4 | 1 + */ + public function testNewHydrationEntityQueryCustomResultSetOrder() + { + // Faked query components + $queryComponents = array( + 'c' => array( + 'metadata' => $this->_em->getClassMetadata('ForumCategory'), + 'parent' => null, + 'relation' => null, + 'map' => null + ), + 'b' => array( + 'metadata' => $this->_em->getClassMetadata('ForumBoard'), + 'parent' => 'c', + 'relation' => $this->_em->getClassMetadata('ForumCategory')->getAssociationMapping('boards'), + 'map' => null + ), + ); + + // Faked table alias map + $tableAliasMap = array( + 'c' => 'c', + 'b' => 'b' + ); + + // Faked result set + $resultSet = array( + array( + 'c__id' => '1', + 'c__position' => '0', + 'c__name' => 'First', + 'b__id' => '1', + 'b__position' => '0', + //'b__category_id' => '1' + ), + 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', + 'c__name' => 'First', + 'b__id' => '3', + 'b__position' => '1', + //'b__category_id' => '1' + ), + array( + 'c__id' => '1', + 'c__position' => '0', + 'c__name' => 'First', + 'b__id' => '4', + 'b__position' => '2', + //'b__category_id' => '1' + ) + ); + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ArrayHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_ARRAY)); + + $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); + $this->assertTrue(is_array($result[0])); + $this->assertTrue(is_array($result[1])); + $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 testResultIteration() { + // Faked query components + $queryComponents = array( + 'u' => array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'map' => null + ) + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u' + ); + + // Faked result set + $resultSet = array( + array( + 'u__id' => '1', + 'u__name' => 'romanb' + ), + array( + 'u__id' => '2', + 'u__name' => 'jwage' + ) + ); + + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ArrayHydrator($this->_em); + + $iterableResult = $hydrator->iterate($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_ARRAY)); + + $rowNum = 0; + while (($row = $iterableResult->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']); + } else if ($rowNum == 1) { + $this->assertEquals(2, $row[0]['id']); + $this->assertEquals('jwage', $row[0]['name']); + } + ++$rowNum; + } + } +} + diff --git a/tests/Orm/Hydration/BasicHydrationTest.php b/tests/Orm/Hydration/BasicHydrationTest.php index 47f629eb2..472ce14e2 100644 --- a/tests/Orm/Hydration/BasicHydrationTest.php +++ b/tests/Orm/Hydration/BasicHydrationTest.php @@ -686,7 +686,7 @@ class Orm_Hydration_BasicHydrationTest extends Doctrine_OrmTestCase $stmt = new Doctrine_HydratorMockStatement($resultSet); $hydrator = new Doctrine_ORM_Internal_Hydration_StandardHydrator($this->_em); - + $result = $hydrator->hydrateResultSet($this->_createParserResult( $stmt, $queryComponents, $tableAliasMap, $hydrationMode, true)); if ($hydrationMode == Doctrine_ORM_Query::HYDRATE_ARRAY) { diff --git a/tests/Orm/Hydration/HydrationTest.php b/tests/Orm/Hydration/HydrationTest.php new file mode 100644 index 000000000..5f95bd31f --- /dev/null +++ b/tests/Orm/Hydration/HydrationTest.php @@ -0,0 +1,34 @@ +_em = $this->_getTestEntityManager(); + } + + /** Helper method */ + protected function _createParserResult($stmt, $queryComponents, $tableToClassAliasMap, + $hydrationMode, $isMixedQuery = false) + { + $parserResult = new Doctrine_ORM_Query_ParserResultDummy(); + $parserResult->setDatabaseStatement($stmt); + $parserResult->setHydrationMode($hydrationMode); + $parserResult->setQueryComponents($queryComponents); + $parserResult->setTableToClassAliasMap($tableToClassAliasMap); + $parserResult->setMixedQuery($isMixedQuery); + return $parserResult; + } +} + diff --git a/tests/Orm/Hydration/ObjectHydratorTest.php b/tests/Orm/Hydration/ObjectHydratorTest.php new file mode 100644 index 000000000..84d7cce64 --- /dev/null +++ b/tests/Orm/Hydration/ObjectHydratorTest.php @@ -0,0 +1,720 @@ + array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'map' => null + ) + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u' + ); + + // Faked result set + $resultSet = array( + array( + 'u__id' => '1', + 'u__name' => 'romanb' + ), + array( + 'u__id' => '2', + 'u__name' => 'jwage' + ) + ); + + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ObjectHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_OBJECT)); + + $this->assertEquals(2, count($result)); + $this->assertTrue($result instanceof Doctrine_ORM_Collection); + $this->assertTrue($result[0] instanceof CmsUser); + $this->assertTrue($result[1] instanceof CmsUser); + $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 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 + */ + public function testNewHydrationMixedQueryFetchJoin() + { + // Faked query components + $queryComponents = array( + 'u' => array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'map' => null, + 'agg' => array('0' => 'nameUpper') + ), + 'p' => array( + 'metadata' => $this->_em->getClassMetadata('CmsPhonenumber'), + 'parent' => 'u', + 'relation' => $this->_em->getClassMetadata('CmsUser')->getAssociationMapping('phonenumbers'), + 'map' => null + ) + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u', + 'p' => 'p' + ); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '42', + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '43', + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'u__0' => 'JWAGE', + 'p__phonenumber' => '91' + ) + ); + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ObjectHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_OBJECT, true)); + + $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); + $this->assertTrue(is_array($result[0])); + $this->assertTrue(is_array($result[1])); + + $this->assertTrue($result[0][0] instanceof CmsUser); + $this->assertTrue($result[0][0]->phonenumbers instanceof Doctrine_ORM_Collection); + $this->assertTrue($result[0][0]->phonenumbers[0] instanceof CmsPhonenumber); + $this->assertTrue($result[0][0]->phonenumbers[1] instanceof CmsPhonenumber); + $this->assertTrue($result[1][0] instanceof CmsUser); + $this->assertTrue($result[1][0]->phonenumbers instanceof Doctrine_ORM_Collection); + + // first user => 2 phonenumbers + $this->assertEquals(2, count($result[0][0]->phonenumbers)); + $this->assertEquals('ROMANB', $result[0]['nameUpper']); + // second user => 1 phonenumber + $this->assertEquals(1, count($result[1][0]->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); + } + + /** + * 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 + */ + public function testNewHydrationMixedQueryNormalJoin() + { + // Faked query components + $queryComponents = array( + 'u' => array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'map' => null + ), + 'p' => array( + 'metadata' => $this->_em->getClassMetadata('CmsPhonenumber'), + 'parent' => 'u', + 'relation' => $this->_em->getClassMetadata('CmsUser')->getAssociationMapping('phonenumbers'), + 'map' => null, + 'agg' => array('0' => 'numPhones') + ) + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u', + 'p' => 'p' + ); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'p__0' => '2', + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'p__0' => '1', + ) + ); + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ObjectHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_OBJECT, true)); + + $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, $result[0]['numPhones']); + // second user => 1 phonenumber + $this->assertEquals(1, $result[1]['numPhones']); + $this->assertTrue($result[0][0] instanceof CmsUser); + $this->assertTrue($result[1][0] instanceof CmsUser); + } + + /** + * 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 + */ + public function testNewHydrationMixedQueryFetchJoinCustomIndex() + { + // Faked query components + $queryComponents = array( + 'u' => array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'agg' => array('0' => 'nameUpper'), + 'map' => 'id' + ), + 'p' => array( + 'metadata' => $this->_em->getClassMetadata('CmsPhonenumber'), + 'parent' => 'u', + 'relation' => $this->_em->getClassMetadata('CmsUser')->getAssociationMapping('phonenumbers'), + 'map' => 'phonenumber' + ) + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u', + 'p' => 'p' + ); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '42', + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '43', + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'u__0' => 'JWAGE', + 'p__phonenumber' => '91' + ) + ); + + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ObjectHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_OBJECT, true)); + + $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); + $this->assertTrue(is_array($result[0])); + $this->assertTrue(is_array($result[1])); + + // test the scalar values + $this->assertEquals('ROMANB', $result[0]['nameUpper']); + $this->assertEquals('JWAGE', $result[1]['nameUpper']); + + $this->assertTrue($result[0]['1'] instanceof CmsUser); + $this->assertTrue($result[1]['2'] instanceof CmsUser); + $this->assertTrue($result[0]['1']->phonenumbers instanceof Doctrine_ORM_Collection); + // first user => 2 phonenumbers. notice the custom indexing by user id + $this->assertEquals(2, count($result[0]['1']->phonenumbers)); + // second user => 1 phonenumber. notice the custom indexing by user id + $this->assertEquals(1, count($result[1]['2']->phonenumbers)); + // test the custom indexing of the phonenumbers + $this->assertTrue(isset($result[0]['1']->phonenumbers['42'])); + $this->assertTrue(isset($result[0]['1']->phonenumbers['43'])); + $this->assertTrue(isset($result[1]['2']->phonenumbers['91'])); + } + + /** + * select u.id, u.status, p.phonenumber, upper(u.name) nameUpper, a.id, a.topic + * from User u + * join u.phonenumbers p + * join u.articles a + * = + * select u.id, u.status, p.phonenumber, upper(u.name) as u__0, a.id, a.topic + * from USERS u + * inner join PHONENUMBERS p ON u.id = p.user_id + * inner join ARTICLES a ON u.id = a.user_id + */ + public function testNewHydrationMixedQueryMultipleFetchJoin() + { + // Faked query components + $queryComponents = array( + 'u' => array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'map' => null, + 'agg' => array('0' => 'nameUpper') + ), + 'p' => array( + 'metadata' => $this->_em->getClassMetadata('CmsPhonenumber'), + 'parent' => 'u', + 'relation' => $this->_em->getClassMetadata('CmsUser')->getAssociationMapping('phonenumbers'), + 'map' => null + ), + 'a' => array( + 'metadata' => $this->_em->getClassMetadata('CmsArticle'), + 'parent' => 'u', + 'relation' => $this->_em->getClassMetadata('CmsUser')->getAssociationMapping('articles'), + 'map' => null + ), + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u', + 'p' => 'p', + 'a' => 'a' + ); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '42', + 'a__id' => '1', + 'a__topic' => 'Getting things done!' + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '43', + 'a__id' => '1', + 'a__topic' => 'Getting things done!' + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '42', + 'a__id' => '2', + 'a__topic' => 'ZendCon' + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '43', + 'a__id' => '2', + 'a__topic' => 'ZendCon' + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'u__0' => 'JWAGE', + 'p__phonenumber' => '91', + 'a__id' => '3', + 'a__topic' => 'LINQ' + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'u__0' => 'JWAGE', + 'p__phonenumber' => '91', + 'a__id' => '4', + 'a__topic' => 'PHP6' + ), + ); + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ObjectHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_OBJECT, true)); + + $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); + $this->assertTrue(is_array($result[0])); + $this->assertTrue(is_array($result[1])); + + $this->assertTrue($result[0][0] instanceof CmsUser); + $this->assertTrue($result[0][0]->phonenumbers instanceof Doctrine_ORM_Collection); + $this->assertTrue($result[0][0]->phonenumbers[0] instanceof CmsPhonenumber); + $this->assertTrue($result[0][0]->phonenumbers[1] instanceof CmsPhonenumber); + $this->assertTrue($result[0][0]->articles instanceof Doctrine_ORM_Collection); + $this->assertTrue($result[0][0]->articles[0] instanceof CmsArticle); + $this->assertTrue($result[0][0]->articles[1] instanceof CmsArticle); + $this->assertTrue($result[1][0] instanceof CmsUser); + $this->assertTrue($result[1][0]->phonenumbers instanceof Doctrine_ORM_Collection); + $this->assertTrue($result[1][0]->phonenumbers[0] instanceof CmsPhonenumber); + $this->assertTrue($result[1][0]->articles[0] instanceof CmsArticle); + $this->assertTrue($result[1][0]->articles[1] instanceof CmsArticle); + } + + /** + * select u.id, u.status, p.phonenumber, upper(u.name) nameUpper, a.id, a.topic, + * c.id, c.topic + * from User u + * join u.phonenumbers p + * join u.articles a + * left join a.comments c + * = + * select u.id, u.status, p.phonenumber, upper(u.name) as u__0, a.id, a.topic, + * c.id, c.topic + * from USERS u + * inner join PHONENUMBERS p ON u.id = p.user_id + * inner join ARTICLES a ON u.id = a.user_id + * left outer join COMMENTS c ON a.id = c.article_id + */ + public function testNewHydrationMixedQueryMultipleDeepMixedFetchJoin() + { + // Faked query components + $queryComponents = array( + 'u' => array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'map' => null, + 'agg' => array('0' => 'nameUpper') + ), + 'p' => array( + 'metadata' => $this->_em->getClassMetadata('CmsPhonenumber'), + 'parent' => 'u', + 'relation' => $this->_em->getClassMetadata('CmsUser')->getAssociationMapping('phonenumbers'), + 'map' => null + ), + 'a' => array( + 'metadata' => $this->_em->getClassMetadata('CmsArticle'), + 'parent' => 'u', + 'relation' => $this->_em->getClassMetadata('CmsUser')->getAssociationMapping('articles'), + 'map' => null + ), + 'c' => array( + 'metadata' => $this->_em->getClassMetadata('CmsComment'), + 'parent' => 'a', + 'relation' => $this->_em->getClassMetadata('CmsArticle')->getAssociationMapping('comments'), + 'map' => null + ), + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u', + 'p' => 'p', + 'a' => 'a', + 'c' => 'c' + ); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '42', + 'a__id' => '1', + 'a__topic' => 'Getting things done!', + 'c__id' => '1', + 'c__topic' => 'First!' + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '43', + 'a__id' => '1', + 'a__topic' => 'Getting things done!', + 'c__id' => '1', + 'c__topic' => 'First!' + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '42', + 'a__id' => '2', + 'a__topic' => 'ZendCon', + 'c__id' => null, + 'c__topic' => null + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'u__0' => 'ROMANB', + 'p__phonenumber' => '43', + 'a__id' => '2', + 'a__topic' => 'ZendCon', + 'c__id' => null, + 'c__topic' => null + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'u__0' => 'JWAGE', + 'p__phonenumber' => '91', + 'a__id' => '3', + 'a__topic' => 'LINQ', + 'c__id' => null, + 'c__topic' => null + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'u__0' => 'JWAGE', + 'p__phonenumber' => '91', + 'a__id' => '4', + 'a__topic' => 'PHP6', + 'c__id' => null, + 'c__topic' => null + ), + ); + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ObjectHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_OBJECT, true)); + + $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); + $this->assertTrue(is_array($result[0])); + $this->assertTrue(is_array($result[1])); + + $this->assertTrue($result[0][0] instanceof CmsUser); + $this->assertTrue($result[1][0] instanceof CmsUser); + // phonenumbers + $this->assertTrue($result[0][0]->phonenumbers instanceof Doctrine_ORM_Collection); + $this->assertTrue($result[0][0]->phonenumbers[0] instanceof CmsPhonenumber); + $this->assertTrue($result[0][0]->phonenumbers[1] instanceof CmsPhonenumber); + $this->assertTrue($result[1][0]->phonenumbers instanceof Doctrine_ORM_Collection); + $this->assertTrue($result[1][0]->phonenumbers[0] instanceof CmsPhonenumber); + // articles + $this->assertTrue($result[0][0]->articles instanceof Doctrine_ORM_Collection); + $this->assertTrue($result[0][0]->articles[0] instanceof CmsArticle); + $this->assertTrue($result[0][0]->articles[1] instanceof CmsArticle); + $this->assertTrue($result[1][0]->articles[0] instanceof CmsArticle); + $this->assertTrue($result[1][0]->articles[1] instanceof CmsArticle); + // article comments + $this->assertTrue($result[0][0]->articles[0]->comments instanceof Doctrine_ORM_Collection); + $this->assertTrue($result[0][0]->articles[0]->comments[0] instanceof CmsComment); + // empty comment collections + $this->assertTrue($result[0][0]->articles[1]->comments instanceof Doctrine_ORM_Collection); + $this->assertEquals(0, count($result[0][0]->articles[1]->comments)); + $this->assertTrue($result[1][0]->articles[0]->comments instanceof Doctrine_ORM_Collection); + $this->assertEquals(0, count($result[1][0]->articles[0]->comments)); + $this->assertTrue($result[1][0]->articles[1]->comments instanceof Doctrine_ORM_Collection); + $this->assertEquals(0, count($result[1][0]->articles[1]->comments)); + } + + /** + * Tests that the hydrator does not rely on a particular order of the rows + * in the result set. + * + * DQL: + * select c.id, c.position, c.name, b.id, b.position + * from ForumCategory c inner join c.boards b + * order by c.position asc, b.position asc + * + * Checks whether the boards are correctly assigned to the categories. + * + * The 'evil' result set that confuses the object population is displayed below. + * + * c.id | c.position | c.name | boardPos | b.id | b.category_id (just for clarity) + * 1 | 0 | First | 0 | 1 | 1 + * 2 | 0 | Second | 0 | 2 | 2 <-- + * 1 | 0 | First | 1 | 3 | 1 + * 1 | 0 | First | 2 | 4 | 1 + */ + public function testNewHydrationEntityQueryCustomResultSetOrder() + { + // Faked query components + $queryComponents = array( + 'c' => array( + 'metadata' => $this->_em->getClassMetadata('ForumCategory'), + 'parent' => null, + 'relation' => null, + 'map' => null + ), + 'b' => array( + 'metadata' => $this->_em->getClassMetadata('ForumBoard'), + 'parent' => 'c', + 'relation' => $this->_em->getClassMetadata('ForumCategory')->getAssociationMapping('boards'), + 'map' => null + ), + ); + + // Faked table alias map + $tableAliasMap = array( + 'c' => 'c', + 'b' => 'b' + ); + + // Faked result set + $resultSet = array( + array( + 'c__id' => '1', + 'c__position' => '0', + 'c__name' => 'First', + 'b__id' => '1', + 'b__position' => '0', + //'b__category_id' => '1' + ), + 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', + 'c__name' => 'First', + 'b__id' => '3', + 'b__position' => '1', + //'b__category_id' => '1' + ), + array( + 'c__id' => '1', + 'c__position' => '0', + 'c__name' => 'First', + 'b__id' => '4', + 'b__position' => '2', + //'b__category_id' => '1' + ) + ); + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ObjectHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_OBJECT)); + + $this->assertEquals(2, count($result)); + $this->assertTrue($result instanceof Doctrine_ORM_Collection); + $this->assertTrue($result[0] instanceof ForumCategory); + $this->assertTrue($result[1] instanceof ForumCategory); + $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 testResultIteration() { + // Faked query components + $queryComponents = array( + 'u' => array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'map' => null + ) + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u' + ); + + // Faked result set + $resultSet = array( + array( + 'u__id' => '1', + 'u__name' => 'romanb' + ), + array( + 'u__id' => '2', + 'u__name' => 'jwage' + ) + ); + + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ObjectHydrator($this->_em); + + $iterableResult = $hydrator->iterate($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_OBJECT)); + + $rowNum = 0; + while (($row = $iterableResult->next()) !== false) { + $this->assertEquals(1, count($row)); + $this->assertTrue($row[0] instanceof CmsUser); + if ($rowNum == 0) { + $this->assertEquals(1, $row[0]->id); + $this->assertEquals('romanb', $row[0]->name); + } else if ($rowNum == 1) { + $this->assertEquals(2, $row[0]->id); + $this->assertEquals('jwage', $row[0]->name); + } + ++$rowNum; + } + } +} + diff --git a/tests/Orm/Hydration/ScalarHydratorTest.php b/tests/Orm/Hydration/ScalarHydratorTest.php new file mode 100644 index 000000000..a91c0cfca --- /dev/null +++ b/tests/Orm/Hydration/ScalarHydratorTest.php @@ -0,0 +1,59 @@ + array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'map' => null + ) + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u' + ); + + // Faked result set + $resultSet = array( + array( + 'u__id' => '1', + 'u__name' => 'romanb' + ), + array( + 'u__id' => '2', + 'u__name' => 'jwage' + ) + ); + + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_ScalarHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_SCALAR)); + + $this->assertTrue(is_array($result)); + $this->assertEquals(2, count($result)); + $this->assertEquals('romanb', $result[0]['u_name']); + $this->assertEquals(1, $result[0]['u_id']); + $this->assertEquals('jwage', $result[1]['u_name']); + $this->assertEquals(2, $result[1]['u_id']); + } +} + diff --git a/tests/Orm/Hydration/SingleScalarHydratorTest.php b/tests/Orm/Hydration/SingleScalarHydratorTest.php new file mode 100644 index 000000000..c4e0b9852 --- /dev/null +++ b/tests/Orm/Hydration/SingleScalarHydratorTest.php @@ -0,0 +1,93 @@ + 'result1', + 'resultSet' => array( + array( + 'u__name' => 'romanb' + ) + )), + // valid + array('name' => 'result2', + 'resultSet' => array( + array( + 'u__id' => '1' + ) + )), + // invalid + array('name' => 'result3', + 'resultSet' => array( + array( + 'u__id' => '1', + 'u__name' => 'romanb' + ) + )), + // invalid + array('name' => 'result4', + 'resultSet' => array( + array( + 'u__id' => '1' + ), + array( + 'u__id' => '2' + ) + )), + ); + } + + /** + * select u.name from CmsUser u where u.id = 1 + * + * @dataProvider singleScalarResultSetProvider + */ + public function testHydrateSingleScalar($name, $resultSet) + { + // Faked query components + $queryComponents = array( + 'u' => array( + 'metadata' => $this->_em->getClassMetadata('CmsUser'), + 'parent' => null, + 'relation' => null, + 'map' => null + ) + ); + + // Faked table alias map + $tableAliasMap = array( + 'u' => 'u' + ); + + $stmt = new Doctrine_HydratorMockStatement($resultSet); + $hydrator = new Doctrine_ORM_Internal_Hydration_SingleScalarHydrator($this->_em); + + if ($name == 'result1') { + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_SINGLE_SCALAR)); + $this->assertEquals('romanb', $result); + } else if ($name == 'result2') { + $result = $hydrator->hydrateAll($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_SINGLE_SCALAR)); + $this->assertEquals(1, $result); + } else if ($name == 'result3' || $name == 'result4') { + try { + $result = $hydrator->hydrateall($stmt, $this->_createParserResult( + $stmt, $queryComponents, $tableAliasMap, Doctrine_ORM_Query::HYDRATE_SINGLE_SCALAR)); + $this->fail(); + } catch (Doctrine_ORM_Exceptions_HydrationException $ex) {} + } + + } +} +