diff --git a/lib/Doctrine.php b/lib/Doctrine.php index 1d91434e7..daeb08253 100644 --- a/lib/Doctrine.php +++ b/lib/Doctrine.php @@ -473,11 +473,11 @@ final class Doctrine const HYDRATE_NONE = 4; /* new hydration modes. move to Query class when it's time. */ - //const HYDRATE_IDENTITY_OBJECT = 1; // default, auto-adds PKs, produces object graphs - //const HYDRATE_IDENTITY_ARRAY = 2; // auto-adds PKs, produces array graphs - //const HYDRATE_SCALAR = 3; // produces flat result list with scalar values - //const HYDRATE_SINGLE_SCALAR = 4; // produces a single scalar value - //const HYDRATE_NONE = 5; // produces a result set as it's returned by the db + const HYDRATE_IDENTITY_OBJECT = 2; // default, auto-adds PKs, produces object graphs + const HYDRATE_IDENTITY_ARRAY = 3; // auto-adds PKs, produces array graphs + const HYDRATE_SCALAR = 5; // produces flat result list with scalar values + const HYDRATE_SINGLE_SCALAR = 6; // produces a single scalar value + //const HYDRATE_NONE = 4; // produces a result set as it's returned by the db /** diff --git a/lib/Doctrine/Connection.php b/lib/Doctrine/Connection.php index 8be3422b5..8dfe6b87b 100644 --- a/lib/Doctrine/Connection.php +++ b/lib/Doctrine/Connection.php @@ -200,8 +200,9 @@ abstract class Doctrine_Connection extends Doctrine_Configurable implements Coun public function __construct($adapter, $user = null, $pass = null) { if (is_object($adapter)) { - if ( ! ($adapter instanceof PDO) && ! in_array('Doctrine_Adapter_Interface', class_implements($adapter))) { - throw new Doctrine_Connection_Exception('First argument should be an instance of PDO or implement Doctrine_Adapter_Interface'); + if ( ! $adapter instanceof PDO) { + throw new Doctrine_Connection_Exception( + 'First argument should be an instance of PDO or implement Doctrine_Adapter_Interface'); } $this->dbh = $adapter; $this->isConnected = true; @@ -216,12 +217,7 @@ abstract class Doctrine_Connection extends Doctrine_Configurable implements Coun if (isset($adapter['other'])) { $this->options['other'] = array(Doctrine::ATTR_PERSISTENT => $adapter['persistent']); } - } - - $this->setAttribute(Doctrine::ATTR_CASE, Doctrine::CASE_NATURAL); - $this->setAttribute(Doctrine::ATTR_ERRMODE, Doctrine::ERRMODE_EXCEPTION); - $this->getAttribute(Doctrine::ATTR_LISTENER)->onOpen($this); } @@ -327,26 +323,16 @@ abstract class Doctrine_Connection extends Doctrine_Configurable implements Coun $this->getListener()->preConnect($event); $e = explode(':', $this->options['dsn']); - $found = false; - if (extension_loaded('pdo')) { if (in_array($e[0], PDO::getAvailableDrivers())) { - $this->dbh = new PDO($this->options['dsn'], $this->options['username'], - $this->options['password'], $this->options['other']); - + $this->dbh = new PDO( + $this->options['dsn'], $this->options['username'], + $this->options['password'], $this->options['other']); $this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $found = true; - } - } - - if ( ! $found) { - $class = 'Doctrine_Adapter_' . ucwords($e[0]); - - if (class_exists($class)) { - $this->dbh = new $class($this->options['dsn'], $this->options['username'], $this->options['password']); - } else { - throw new Doctrine_Connection_Exception("Couldn't locate driver named " . $e[0]); + $this->dbh->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER); } + } else { + throw new Doctrine_Connection_Exception("Couldn't locate driver named " . $e[0]); } // attach the pending attributes to adapter diff --git a/lib/Doctrine/EntityManager.php b/lib/Doctrine/EntityManager.php new file mode 100644 index 000000000..823175d35 --- /dev/null +++ b/lib/Doctrine/EntityManager.php @@ -0,0 +1,551 @@ +. + */ + +/** + * The EntityManager is a central access point to ORM functionality. + * + * @package Doctrine + * @subpackage EntityManager + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.phpdoctrine.org + * @since 2.0 + * @version $Revision$ + * @author Roman Borschel + * @todo package:orm + */ +class Doctrine_EntityManager +{ + /** + * The unique name of the EntityManager. The name is used to bind entity classes + * to certain EntityManagers. + * + * @var string + */ + private $_name; + + /** + * The database connection used by the EntityManager. + * + * @var Doctrine_Connection + */ + private $_conn; + + /** + * Flush modes enumeration. + */ + private static $_flushModes = array( + // auto: Flush occurs automatically after each operation that issues database + // queries. No operations are queued. + 'auto', + // commit: Flush occurs automatically at transaction commit. + 'commit', + // manual: Flush occurs never automatically. + 'manual' + ); + + /** + * The metadata factory, used to retrieve the metadata of entity classes. + * + * @var Doctrine_ClassMetadata_Factory + */ + private $_metadataFactory; + + /** + * The EntityPersister instances. + * @todo Implementation. + * + * @var array + */ + private $_persisters = array(); + + /** + * The EntityRepository instances. + * + * @var array + */ + private $_repositories = array(); + + /** + * The currently used flush mode. Defaults to 'commit'. + * + * @var string + */ + private $_flushMode = 'commit'; + + /** + * Map of all EntityManagers, keys are the names. + * + * @var array + */ + private static $_ems = array(); + + /** + * EntityManager to Entity bindings. + * + * @var array + */ + private static $_emBindings = array(); + + /** + * The unit of work. + * + * @var UnitOfWork + */ + private $_unitOfWork; + + /** + * Enter description here... + * + * @var unknown_type + */ + //private $_dataTemplates = array(); + + /** + * Creates a new EntityManager that operates on the given database connection. + * + * @param Doctrine_Connection $conn + * @param string $name + */ + public function __construct(Doctrine_Connection $conn, $name = null) + { + $this->_conn = $conn; + $this->_name = $name; + $this->_metadataFactory = new Doctrine_ClassMetadata_Factory($this, + new Doctrine_ClassMetadata_CodeDriver()); + $this->_unitOfWork = new Doctrine_Connection_UnitOfWork($conn); + if ($name !== null) { + self::$_ems[$name] = $this; + } else { + self::$_ems[] = $this; + } + } + + /** + * Gets the EntityManager that is responsible for the Entity. + * + * @param string $entityName + * @return EntityManager + * @throws Doctrine_EntityManager_Exception If a suitable manager can not be found. + */ + public static function getManager($entityName = null) + { + if ( ! is_null($entityName) && isset(self::$_emBindings[$entityName])) { + $emName = self::$_emBindings[$entityName]; + if (isset(self::$_ems[$emName])) { + return self::$_ems[$emName]; + } else { + throw Doctrine_EntityManager_Exception::noManagerWithName($emName); + } + } else if (self::$_ems) { + return current(self::$_ems); + } else { + throw Doctrine_EntityManager_Exception::noEntityManagerAvailable(); + } + } + + /** + * Enter description here... + * + * @param unknown_type $entityName + * @param unknown_type $emName + */ + public static function bindEntityToManager($entityName, $emName) + { + if (isset(self::$_emBindings[$entityName])) { + throw Doctrine_EntityManager_Exception::entityAlreadyBound($entityName); + } + self::$_emBindings[$entityName] = $emName; + } + + /** + * Clears all bindings between Entities and EntityManagers. + */ + public static function unbindAllManagers() + { + self::$_emBindings = array(); + } + + /** + * Releases all EntityManagers. + * + */ + public static function releaseAllManagers() + { + self::$_ems = array(); + } + + /** + * Gets the database connection object used by the EntityManager. + * + * @return Doctrine_Connection + */ + public function getConnection() + { + return $this->_conn; + } + + /** + * Returns the metadata for a class. Alias for getClassMetadata(). + * + * @return Doctrine_Metadata + * @todo package:orm + */ + public function getMetadata($className) + { + return $this->getClassMetadata($className); + } + + /** + * Returns the metadata for a class. + * + * @return Doctrine_Metadata + */ + public function getClassMetadata($className) + { + return $this->_metadataFactory->getMetadataFor($className); + } + + /** + * Sets the driver that is used to obtain metadata informations about entity + * classes. + * + * @param $driver The driver to use. + */ + public function setClassMetadataDriver($driver) + { + $this->_metadataFactory->setDriver($driver); + } + + /** + * Creates a new Doctrine_Query object that operates on this connection. + * + * @return Doctrine_Query + * @todo package:orm + */ + public function createQuery($dql = "") + { + $query = new Doctrine_Query($this); + if ( ! empty($dql)) { + $query->parseQuery($dql); + } + + return $query; + } + + /** + * Enter description here... + * + * @param unknown_type $entityName + * @return unknown + */ + public function getEntityPersister($entityName) + { + if ( ! isset($this->_persisters[$entityName])) { + $class = $this->getClassMetadata($entityName); + if ($class->getInheritanceType() == Doctrine::INHERITANCE_TYPE_JOINED) { + $persister = new Doctrine_EntityPersister_JoinedSubclass($this, $class); + } else { + $persister = new Doctrine_EntityPersister_Standard($this, $class); + } + $this->_persisters[$entityName] = $persister; + } + return $this->_persisters[$entityName]; + } + + /** + * Detaches an entity from the manager. It's lifecycle is no longer managed. + * + * @param Doctrine_Entity $entity + * @return unknown + */ + public function detach(Doctrine_Entity $entity) + { + return $this->_unitOfWork->unregisterIdentity($entity); + } + + /** + * Returns the current internal transaction nesting level. + * + * @return integer The nesting level. A value of 0 means theres no active transaction. + * @todo package:orm??? + */ + public function getInternalTransactionLevel() + { + return $this->transaction->getInternalTransactionLevel(); + } + + /** + * Initiates a transaction. + * + * This method must only be used by Doctrine itself to initiate transactions. + * Userland-code must use {@link beginTransaction()}. + * + * @todo package:orm??? + */ + public function beginInternalTransaction($savepoint = null) + { + return $this->transaction->beginInternalTransaction($savepoint); + } + + /** + * Creates a query with the specified name. + * + * @todo Implementation. + * @throws SomeException If there is no query registered with the given name. + */ + public function createNamedQuery($name) + { + //... + } + + /** + * @todo Implementation. + */ + public function createNativeQuery($sql = "") + { + //... + } + + /** + * @todo Implementation. + */ + public function createNamedNativeQuery($name) + { + //... + } + + /** + * @todo Implementation. + */ + public function createCriteria() + { + //... + } + + /** + * Flushes all changes to objects that have been queued up to now to the database. + * + * @todo package:orm + */ + public function flush() + { + $this->beginInternalTransaction(); + $this->_unitOfWork->flush(); + $this->commit(); + } + + /** + * Sets the flush mode. + * + * @param string $flushMode + */ + public function setFlushMode($flushMode) + { + if ( ! in_array($flushMode, self::$_flushModes)) { + throw Doctrine_EntityManager_Exception::invalidFlushMode(); + } + $this->_flushMode = $flushMode; + } + + /** + * Gets the currently used flush mode. + * + * @return string + */ + public function getFlushMode() + { + return $this->_flushMode; + } + + /** + * Clears the persistence context, detaching all entities. + * + * @return void + * @todo package:orm + */ + public function clear($entityName = null) + { + if ($entityName === null) { + $this->_unitOfWork->detachAll(); + foreach ($this->_mappers as $mapper) { + $mapper->clear(); // clear identity map of each mapper + } + } else { + $this->getMapper($entityName)->clear(); + } + } + + /** + * Releases the EntityManager. + * + */ + public function close() + { + + } + + /** + * getResultCacheDriver + * + * @return Doctrine_Cache_Interface + * @todo package:orm + */ + public function getResultCacheDriver() + { + if ( ! $this->getAttribute(Doctrine::ATTR_RESULT_CACHE)) { + throw new Doctrine_Exception('Result Cache driver not initialized.'); + } + + return $this->getAttribute(Doctrine::ATTR_RESULT_CACHE); + } + + /** + * getQueryCacheDriver + * + * @return Doctrine_Cache_Interface + * @todo package:orm + */ + public function getQueryCacheDriver() + { + if ( ! $this->getAttribute(Doctrine::ATTR_QUERY_CACHE)) { + throw new Doctrine_Exception('Query Cache driver not initialized.'); + } + + return $this->getAttribute(Doctrine::ATTR_QUERY_CACHE); + } + + /** + * Saves the given entity, persisting it's state. + */ + public function save(Doctrine_Entity $entity) + { + //... + } + + /** + * Removes the given entity from the persistent store. + */ + public function delete(Doctrine_Entity $entity) + { + //... + } + + /** + * Gets the repository for the given entity name. + * + * @return Doctrine_EntityRepository The repository. + * @todo Implementation. + */ + public function getRepository($entityName) + { + if (isset($this->_repositories[$entityName])) { + return $this->_repositories[$entityName]; + } + + $metadata = $this->getClassMetadata($entityName); + $customRepositoryClassName = $metadata->getCustomRepositoryClass(); + if ($customRepositoryClassName !== null) { + $repository = new $customRepositoryClassName($entityName, $metadata); + } else { + $repository = new Doctrine_EntityRepository($entityName, $metadata); + } + $this->_repositories[$entityName] = $repository; + + return $repository; + } + + /** + * Creates an entity. Used to reconstitution as well as new creation. + * + * @param + * @param + * @return Doctrine_Entity + */ + public function createEntity($className, array $data) + { + $className = $this->_getClassnameToReturn($data, $className); + $classMetadata = $this->getClassMetadata($className); + if ( ! empty($data)) { + $identifierFieldNames = $classMetadata->getIdentifier(); + $isNew = false; + foreach ($identifierFieldNames as $fieldName) { + if ( ! isset($data[$fieldName])) { + // id field not found return new entity + $isNew = true; + break; + } + $id[] = $data[$fieldName]; + } + if ($isNew) { + return new $className(true, $data); + } + + $idHash = $this->_unitOfWork->getIdentifierHash($id); + + if ($entity = $this->_unitOfWork->tryGetByIdHash($idHash, + $classMetadata->getRootClassName())) { + return $entity; + } else { + $entity = new $className(false, $data); + $this->_unitOfWork->registerIdentity($entity); + } + $data = array(); + } else { + $entity = new $className(true, $data); + } + + return $entity; + } + + /** + * Check the dataset for a discriminator column to determine the correct + * class to instantiate. If no discriminator column is found, the given + * classname will be returned. + * + * @return string The name of the class to instantiate. + * @todo Can be optimized performance-wise. + * @todo Move to EntityManager::createEntity() + */ + private function _getClassnameToReturn(array $data, $className) + { + $class = $this->getClassMetadata($className); + + $discCol = $class->getInheritanceOption('discriminatorColumn'); + if ( ! $discCol) { + return $className; + } + + $discMap = $class->getInheritanceOption('discriminatorMap'); + + if (isset($data[$discCol], $discMap[$data[$discCol]])) { + return $discMap[$data[$discCol]]; + } else { + return $className; + } + } + + public function getUnitOfWork() + { + return $this->_unitOfWork; + } +} + +?> \ No newline at end of file diff --git a/lib/Doctrine/EntityManager/Exception.php b/lib/Doctrine/EntityManager/Exception.php new file mode 100644 index 000000000..d39346e01 --- /dev/null +++ b/lib/Doctrine/EntityManager/Exception.php @@ -0,0 +1,24 @@ +. + */ + +/** + * + * + * @author Roman Borschel + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @version $Revision: 3406 $ + * @link www.phpdoctrine.org + * @since 2.0 + */ +abstract class Doctrine_EntityPersister_Abstract +{ + /** + * The names of all the fields that are available on entities created by this mapper. + */ + protected $_fieldNames = array(); + + /** + * Metadata object that descibes the mapping of the mapped entity class. + * + * @var Doctrine_ClassMetadata + */ + protected $_classMetadata; + + /** + * The name of the domain class this mapper is used for. + */ + protected $_domainClassName; + + /** + * The Doctrine_Connection object that the database connection of this mapper. + * + * @var Doctrine_Connection $conn + */ + protected $_conn; + + /** + * The EntityManager. + * + * @var unknown_type + */ + protected $_em; + + /** + * The concrete mapping strategy that is used. + */ + protected $_mappingStrategy; + + /** + * Null object. + */ + private $_nullObject; + + /** + * A list of registered entity listeners. + */ + private $_entityListeners = array(); + + /** + * Enter description here... + * + * @var unknown_type + * @todo To EntityManager. + */ + private $_dataTemplate = array(); + + + /** + * Constructs a new mapper. + * + * @param string $name The name of the domain class this mapper is used for. + * @param Doctrine_Table $table The table object used for the mapping procedure. + * @throws Doctrine_Connection_Exception if there are no opened connections + */ + public function __construct(Doctrine_EntityManager $em, Doctrine_ClassMetadata $classMetadata) + { + $this->_em = $em; + $this->_domainClassName = $classMetadata->getClassName(); + $this->_conn = $classMetadata->getConnection(); + $this->_classMetadata = $classMetadata; + $this->_nullObject = Doctrine_Null::$INSTANCE; + } + + /** + * Assumes that the keys of the given field array are field names and converts + * them to column names. + * + * @return array + */ + protected function _convertFieldToColumnNames(array $fields, Doctrine_ClassMetadata $class) + { + $converted = array(); + foreach ($fields as $fieldName => $value) { + $converted[$class->getColumnName($fieldName)] = $value; + } + + return $converted; + } + + /** + * deletes all related composites + * this method is always called internally when a record is deleted + * + * @throws PDOException if something went wrong at database level + * @return void + */ + protected function _deleteComposites(Doctrine_Entity $record) + { + $classMetadata = $this->_classMetadata; + foreach ($classMetadata->getRelations() as $fk) { + if ($fk->isComposite()) { + $obj = $record->get($fk->getAlias()); + if ($obj instanceof Doctrine_Entity && + $obj->state() != Doctrine_Entity::STATE_LOCKED) { + $obj->delete($this->_mapper->getConnection()); + } + } + } + } + + /** + * sets the connection for this class + * + * @params Doctrine_Connection a connection object + * @return Doctrine_Table this object + * @todo refactor + */ + /*public function setConnection(Doctrine_Connection $conn) + { + $this->_conn = $conn; + return $this; + }*/ + + /** + * Returns the connection the mapper is currently using. + * + * @return Doctrine_Connection|null The connection object. + */ + public function getConnection() + { + return $this->_conn; + } + + public function getEntityManager() + { + return $this->_em; + } + + /** + * creates a new record + * + * @param $array an array where keys are field names and + * values representing field values + * @return Doctrine_Entity the created record object + * @todo To EntityManager. + */ + public function create(array $array = array()) + { + $record = new $this->_domainClassName(); + $record->fromArray($array); + + return $record; + } + + public function addEntityListener(Doctrine_Record_Listener $listener) + { + if ( ! in_array($listener, $this->_entityListeners)) { + $this->_entityListeners[] = $listener; + return true; + } + return false; + } + + public function removeEntityListener(Doctrine_Record_Listener $listener) + { + if ($key = array_search($listener, $this->_entityListeners, true)) { + unset($this->_entityListeners[$key]); + return true; + } + return false; + } + + public function notifyEntityListeners(Doctrine_Entity $entity, $callback, $eventType) + { + if ($this->_entityListeners) { + $event = new Doctrine_Event($entity, $eventType); + foreach ($this->_entityListeners as $listener) { + $listener->$callback($event); + } + } + } + + /** + * Enter description here... + * + * @param Doctrine_Entity $entity + * @return unknown + * @todo To EntityManager + */ + public function detach(Doctrine_Entity $entity) + { + return $this->_conn->getUnitOfWork()->detach($entity); + } + + /** + * clear + * clears the first level cache (identityMap) + * + * @return void + * @todo what about a more descriptive name? clearIdentityMap? + * @todo To EntityManager + */ + public function clear() + { + $this->_conn->getUnitOfWork()->clearIdentitiesForEntity($this->_classMetadata->getRootClassName()); + } + + /** + * addRecord + * adds a record to identity map + * + * @param Doctrine_Entity $record record to be added + * @return boolean + * @todo Better name? registerRecord? Move elsewhere to the new location of the identity maps. + * @todo Remove. + */ + public function addRecord(Doctrine_Entity $record) + { + if ($this->_conn->unitOfWork->contains($record)) { + return false; + } + $this->_conn->unitOfWork->registerIdentity($record); + + return true; + } + + /** + * Tells the mapper to manage the entity if it's not already managed. + * + * @return boolean TRUE if the entity was previously not managed and is now managed, + * FALSE otherwise (the entity is already managed). + * @todo Remove. + */ + public function manage(Doctrine_Entity $record) + { + return $this->_conn->unitOfWork->manage($record); + } + + /** + * removeRecord + * removes a record from the identity map, returning true if the record + * was found and removed and false if the record wasn't found. + * + * @param Doctrine_Entity $record record to be removed + * @return boolean + * @todo Move elsewhere to the new location of the identity maps. + */ + public function removeRecord(Doctrine_Entity $record) + { + if ($this->_conn->unitOfWork->contains($record)) { + $this->_conn->unitOfWork->unregisterIdentity($record); + return true; + } + + return false; + } + + /** + * getRecord + * First checks if record exists in identityMap, if not + * returns a new record. + * + * @return Doctrine_Entity + * @todo To EntityManager. + */ + public function getRecord(array $data) + { + if ( ! empty($data)) { + $identifierFieldNames = $this->_classMetadata->getIdentifier(); + + $found = false; + foreach ($identifierFieldNames as $fieldName) { + if ( ! isset($data[$fieldName])) { + // primary key column not found return new record + $found = true; + break; + } + $id[] = $data[$fieldName]; + } + + if ($found) { + return new $this->_domainClassName(true, $data); + } + + $idHash = $this->_conn->unitOfWork->getIdentifierHash($id); + + if ($record = $this->_conn->unitOfWork->tryGetByIdHash($idHash, + $this->_classMetadata->getRootClassName())) { + $record->hydrate($data); + } else { + $record = new $this->_domainClassName(false, $data); + $this->_conn->unitOfWork->registerIdentity($record); + } + $data = array(); + } else { + $record = new $this->_domainClassName(true, $data); + } + + return $record; + } + + /** + * @param $id database row id + * @todo Looks broken. Figure out an implementation and decide whether its needed. + */ + final public function getProxy($id = null) + { + if ($id !== null) { + $identifierColumnNames = $this->_classMetadata->getIdentifierColumnNames(); + $query = 'SELECT ' . implode(', ', $identifierColumnNames) + . ' FROM ' . $this->_classMetadata->getTableName() + . ' WHERE ' . implode(' = ? && ', $identifierColumnNames) . ' = ?'; + $query = $this->applyInheritance($query); + + $params = array_merge(array($id),array()); + + $data = $this->_conn->execute($query, $params)->fetch(PDO::FETCH_ASSOC); + + if ($data === false) { + return false; + } + } + + return $this->getRecord($data); + } + + /** + * applyInheritance + * @param $where query where part to be modified + * @return string query where part with column aggregation inheritance added + * @todo What to do with this? Remove if possible. + */ + final public function applyInheritance($where) + { + $discCol = $this->_classMetadata->getInheritanceOption('discriminatorColumn'); + if ( ! $discCol) { + return $where; + } + + $discMap = $this->_classMetadata->getInheritanceOption('discriminatorMap'); + $inheritanceMap = array($discCol => array_search($this->_domainClassName, $discMap)); + if ( ! empty($inheritanceMap)) { + $a = array(); + foreach ($inheritanceMap as $column => $value) { + $a[] = $column . ' = ?'; + } + $i = implode(' AND ', $a); + $where .= ' AND ' . $i; + } + + return $where; + } + + /** + * prepareValue + * 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 + * + * + * @throws Doctrine_Table_Exception if unserialization of array/object typed column fails or + * @throws Doctrine_Table_Exception if uncompression of gzip typed column fails * + * @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 To EntityManager. Make private and use in createEntity(). + * .. Or, maybe better: Move to hydrator for performance reasons. + */ + public function prepareValue($fieldName, $value, $typeHint = null) + { + if ($value === $this->_nullObject) { + return $this->_nullObject; + } else if ($value === null) { + return null; + } else { + $type = is_null($typeHint) ? $this->_classMetadata->getTypeOf($fieldName) : $typeHint; + switch ($type) { + case 'integer': + case 'string'; + // don't do any casting here PHP INT_MAX is smaller than what the databases support + break; + case 'enum': + return $this->_classMetadata->enumValue($fieldName, $value); + break; + case 'boolean': + return (boolean) $value; + break; + case 'array': + case 'object': + if (is_string($value)) { + $value = unserialize($value); + if ($value === false) { + throw new Doctrine_Mapper_Exception('Unserialization of ' . $fieldName . ' failed.'); + } + return $value; + } + break; + case 'gzip': + $value = gzuncompress($value); + if ($value === false) { + throw new Doctrine_Mapper_Exception('Uncompressing of ' . $fieldName . ' failed.'); + } + return $value; + break; + } + } + return $value; + } + + /** + * getComponentName + * + * @return void + * @deprecated Use getMappedClassName() + */ + public function getComponentName() + { + return $this->_domainClassName; + } + + /** + * Gets the name of the class the mapper is used for. + */ + public function getMappedClassName() + { + return $this->_domainClassName; + } + + /** + * Saves an entity and all it's related entities. + * + * @param Doctrine_Entity $record The entity to save. + * @param Doctrine_Connection $conn The connection to use. Will default to the mapper's + * connection. + * @throws Doctrine_Mapper_Exception If the mapper is unable to save the given entity. + */ + public function save(Doctrine_Entity $record, Doctrine_Connection $conn = null) + { + if ( ! ($record instanceof $this->_domainClassName)) { + throw new Doctrine_Mapper_Exception("Mapper of type " . $this->_domainClassName . " + can't save instances of type" . get_class($record) . "."); + } + + if ($conn === null) { + $conn = $this->_conn; + } + + $state = $record->state(); + if ($state === Doctrine_Entity::STATE_LOCKED) { + return false; + } + + $record->state(Doctrine_Entity::STATE_LOCKED); + + try { + $conn->beginInternalTransaction(); + $saveLater = $this->_saveRelated($record); + + $record->state($state); + + if ($record->isValid()) { + $this->_insertOrUpdate($record); + } else { + $conn->transaction->addInvalid($record); + } + + $state = $record->state(); + $record->state(Doctrine_Entity::STATE_LOCKED); + + foreach ($saveLater as $fk) { + $alias = $fk->getAlias(); + if ($record->hasReference($alias)) { + $obj = $record->$alias; + // check that the related object is not an instance of Doctrine_Null + if ( ! ($obj instanceof Doctrine_Null)) { + $obj->save($conn); + } + } + } + + // save the MANY-TO-MANY associations + $this->saveAssociations($record); + // reset state + $record->state($state); + $conn->commit(); + } catch (Exception $e) { + $conn->rollback(); + throw $e; + } + + return true; + } + + /** + * Inserts or updates an entity, depending on it's state. + * + * @param Doctrine_Entity $record The entity to insert/update. + */ + protected function _insertOrUpdate(Doctrine_Entity $record) + { + $record->preSave(); + $this->notifyEntityListeners($record, 'preSave', Doctrine_Event::RECORD_SAVE); + + switch ($record->state()) { + case Doctrine_Entity::STATE_TDIRTY: + $this->_insert($record); + break; + case Doctrine_Entity::STATE_DIRTY: + case Doctrine_Entity::STATE_PROXY: + $this->_update($record); + break; + case Doctrine_Entity::STATE_CLEAN: + case Doctrine_Entity::STATE_TCLEAN: + // do nothing + break; + } + + $record->postSave(); + $this->notifyEntityListeners($record, 'postSave', Doctrine_Event::RECORD_SAVE); + } + + /** + * saves the given record + * + * @param Doctrine_Entity $record + * @return void + */ + public function saveSingleRecord(Doctrine_Entity $record) + { + $this->_insertOrUpdate($record); + } + + /** + * _saveRelated + * saves all related records to $record + * + * @throws PDOException if something went wrong at database level + * @param Doctrine_Entity $record + */ + protected function _saveRelated(Doctrine_Entity $record) + { + $saveLater = array(); + foreach ($record->getReferences() as $k => $v) { + $rel = $record->getTable()->getRelation($k); + + $local = $rel->getLocal(); + $foreign = $rel->getForeign(); + + if ($rel instanceof Doctrine_Relation_ForeignKey) { + $saveLater[$k] = $rel; + } else if ($rel instanceof Doctrine_Relation_LocalKey) { + // ONE-TO-ONE relationship + $obj = $record->get($rel->getAlias()); + + // Protection against infinite function recursion before attempting to save + if ($obj instanceof Doctrine_Entity && $obj->isModified()) { + $obj->save($this->_conn); + + /** Can this be removed? + $id = array_values($obj->identifier()); + + foreach ((array) $rel->getLocal() as $k => $field) { + $record->set($field, $id[$k]); + } + */ + } + } + } + + return $saveLater; + } + + /** + * saveAssociations + * + * this method takes a diff of one-to-many / many-to-many original and + * current collections and applies the changes + * + * for example if original many-to-many related collection has records with + * primary keys 1,2 and 3 and the new collection has records with primary keys + * 3, 4 and 5, this method would first destroy the associations to 1 and 2 and then + * save new associations to 4 and 5 + * + * @throws Doctrine_Connection_Exception if something went wrong at database level + * @param Doctrine_Entity $record + * @return void + */ + public function saveAssociations(Doctrine_Entity $record) + { + foreach ($record->getReferences() as $relationName => $relatedObject) { + if ($relatedObject === Doctrine_Null::$INSTANCE) { + continue; + } + $rel = $record->getTable()->getRelation($relationName); + + if ($rel instanceof Doctrine_Relation_Association) { + $relatedObject->save($this->_conn); + $assocTable = $rel->getAssociationTable(); + + foreach ($relatedObject->getDeleteDiff() as $r) { + $query = 'DELETE FROM ' . $assocTable->getTableName() + . ' WHERE ' . $rel->getForeign() . ' = ?' + . ' AND ' . $rel->getLocal() . ' = ?'; + // FIXME: composite key support + $ids1 = $r->identifier(); + $id1 = count($ids1) > 0 ? array_pop($ids1) : null; + $ids2 = $record->identifier(); + $id2 = count($ids2) > 0 ? array_pop($ids2) : null; + $this->_conn->execute($query, array($id1, $id2)); + } + + $assocMapper = $this->_conn->getMapper($assocTable->getComponentName()); + foreach ($relatedObject->getInsertDiff() as $r) { + $assocRecord = $assocMapper->create(); + $assocRecord->set($assocTable->getFieldName($rel->getForeign()), $r); + $assocRecord->set($assocTable->getFieldName($rel->getLocal()), $record); + $assocMapper->save($assocRecord); + } + } + } + } + + /** + * Updates an entity. + * + * @param Doctrine_Entity $record record to be updated + * @return boolean whether or not the update was successful + * @todo Move to Doctrine_Table (which will become Doctrine_Mapper). + */ + protected function _update(Doctrine_Entity $record) + { + $record->preUpdate(); + $this->notifyEntityListeners($record, 'preUpdate', Doctrine_Event::RECORD_UPDATE); + + $table = $this->_classMetadata; + $this->_doUpdate($record); + + $record->postUpdate(); + $this->notifyEntityListeners($record, 'postUpdate', Doctrine_Event::RECORD_UPDATE); + + return true; + } + + abstract protected function _doUpdate(Doctrine_Entity $entity); + + /** + * Inserts an entity. + * + * @param Doctrine_Entity $record record to be inserted + * @return boolean + */ + protected function _insert(Doctrine_Entity $record) + { + $record->preInsert(); + $this->notifyEntityListeners($record, 'preInsert', Doctrine_Event::RECORD_INSERT); + + $this->_doInsert($record); + $this->addRecord($record); + + $record->postInsert(); + $this->notifyEntityListeners($record, 'postInsert', Doctrine_Event::RECORD_INSERT); + + return true; + } + + abstract protected function _doInsert(Doctrine_Entity $entity); + + /** + * Deletes given entity and all it's related entities. + * + * Triggered Events: onPreDelete, onDelete. + * + * @return boolean true on success, false on failure + * @throws Doctrine_Mapper_Exception + */ + public function delete(Doctrine_Entity $record, Doctrine_Connection $conn = null) + { + if ( ! $record->exists()) { + return false; + } + + if ( ! ($record instanceof $this->_domainClassName)) { + throw new Doctrine_Mapper_Exception("Mapper of type " . $this->_domainClassName . " + can't save instances of type" . get_class($record) . "."); + } + + if ($conn == null) { + $conn = $this->_conn; + } + + $record->preDelete(); + $this->notifyEntityListeners($record, 'preDelete', Doctrine_Event::RECORD_DELETE); + + $table = $this->_classMetadata; + + $state = $record->state(); + $record->state(Doctrine_Entity::STATE_LOCKED); + + $this->_doDelete($record); + + $record->postDelete(); + $this->notifyEntityListeners($record, 'postDelete', Doctrine_Event::RECORD_DELETE); + + return true; + } + + abstract protected function _doDelete(Doctrine_Entity $entity); + + /** + * Inserts a row into a table. + * + * @todo This method could be used to allow mapping to secondary table(s). + * @see http://www.oracle.com/technology/products/ias/toplink/jpa/resources/toplink-jpa-annotations.html#SecondaryTable + */ + protected function _insertRow($tableName, array $data) + { + $this->_conn->insert($tableName, $data); + } + + /** + * Deletes rows of a table. + * + * @todo This method could be used to allow mapping to secondary table(s). + * @see http://www.oracle.com/technology/products/ias/toplink/jpa/resources/toplink-jpa-annotations.html#SecondaryTable + */ + protected function _deleteRow($tableName, array $identifierToMatch) + { + $this->_conn->delete($tableName, $identifierToMatch); + } + + /** + * Deletes rows of a table. + * + * @todo This method could be used to allow mapping to secondary table(s). + * @see http://www.oracle.com/technology/products/ias/toplink/jpa/resources/toplink-jpa-annotations.html#SecondaryTable + */ + protected function _updateRow($tableName, array $data, array $identifierToMatch) + { + $this->_conn->update($tableName, $data, $identifierToMatch); + } + + public function getClassMetadata() + { + return $this->_classMetadata; + } + + public function getFieldName($columnName) + { + return $this->_classMetadata->getFieldName($columnName); + } + + public function getFieldNames() + { + if ($this->_fieldNames) { + return $this->_fieldNames; + } + $this->_fieldNames = $this->_classMetadata->getFieldNames(); + return $this->_fieldNames; + } + + public function getOwningClass($fieldName) + { + return $this->_classMetadata; + } + + /* Hooks used during SQL query construction to manipulate the query. */ + + /** + * Callback that is invoked during the SQL construction process. + */ + public function getCustomJoins() + { + return array(); + } + + /** + * Callback that is invoked during the SQL construction process. + */ + public function getCustomFields() + { + return array(); + } +} diff --git a/lib/Doctrine/EntityPersister/Exception.php b/lib/Doctrine/EntityPersister/Exception.php new file mode 100644 index 000000000..d398e8eab --- /dev/null +++ b/lib/Doctrine/EntityPersister/Exception.php @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/lib/Doctrine/EntityPersister/JoinedSubclass.php b/lib/Doctrine/EntityPersister/JoinedSubclass.php new file mode 100644 index 000000000..65c9b8b5e --- /dev/null +++ b/lib/Doctrine/EntityPersister/JoinedSubclass.php @@ -0,0 +1,318 @@ +. + */ + +/** + * The joined mapping strategy maps a single entity instance to several tables in the + * database as it is defined by Class Table Inheritance. + * + * @author Roman Borschel + * @package Doctrine + * @subpackage JoinedSubclass + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @version $Revision$ + * @link www.phpdoctrine.org + * @since 2.0 + */ +class Doctrine_EntityPersister_JoinedSubclass extends Doctrine_EntityPersister_Abstract +{ + protected $_columnNameFieldNameMap = array(); + + /** + * Inserts an entity that is part of a Class Table Inheritance hierarchy. + * + * @param Doctrine_Entity $record record to be inserted + * @return boolean + */ + protected function _doInsert(Doctrine_Entity $record) + { + $class = $this->_classMetadata; + $conn = $this->_conn; + + $dataSet = $this->_groupFieldsByDefiningClass($record); + $component = $class->getClassName(); + $classes = $class->getParentClasses(); + array_unshift($classes, $component); + + try { + $conn->beginInternalTransaction(); + $identifier = null; + foreach (array_reverse($classes) as $k => $parent) { + $parentClass = $conn->getClassMetadata($parent); + if ($k == 0) { + $identifierType = $parentClass->getIdentifierType(); + if ($identifierType == Doctrine::IDENTIFIER_AUTOINC) { + $this->_insertRow($parentClass->getTableName(), $dataSet[$parent]); + $identifier = $conn->sequence->lastInsertId(); + } else if ($identifierType == Doctrine::IDENTIFIER_SEQUENCE) { + $seq = $record->getClassMetadata()->getTableOption('sequenceName'); + if ( ! empty($seq)) { + $id = $conn->sequence->nextId($seq); + $identifierFields = (array)$parentClass->getIdentifier(); + $dataSet[$parent][$identifierFields[0]] = $id; + $this->_insertRow($parentClass->getTableName(), $dataSet[$parent]); + } + } else { + throw new Doctrine_Mapper_Exception("Unsupported identifier type '$identifierType'."); + } + $record->assignIdentifier($identifier); + } else { + foreach ((array) $record->identifier() as $id => $value) { + $dataSet[$parent][$parentClass->getColumnName($id)] = $value; + } + $this->_insertRow($parentClass->getTableName(), $dataSet[$parent]); + } + } + $conn->commit(); + } catch (Exception $e) { + $conn->rollback(); + throw $e; + } + + return true; + } + + /** + * Updates an entity that is part of a Class Table Inheritance hierarchy. + * + * @param Doctrine_Entity $record record to be updated + * @return boolean whether or not the update was successful + * @todo Move to Doctrine_Table (which will become Doctrine_Mapper). + */ + protected function _doUpdate(Doctrine_Entity $record) + { + $conn = $this->_conn; + $classMetadata = $this->_classMetadata; + $identifier = $this->_convertFieldToColumnNames($record->identifier(), $classMetadata); + $dataSet = $this->_groupFieldsByDefiningClass($record); + $component = $classMetadata->getClassName(); + $classes = $classMetadata->getParentClasses(); + array_unshift($classes, $component); + + foreach ($record as $field => $value) { + if ($value instanceof Doctrine_Entity) { + if ( ! $value->exists()) { + $value->save(); + } + $idValues = $value->identifier(); + $record->set($field, $idValues[0]); + } + } + + foreach (array_reverse($classes) as $class) { + $parentTable = $conn->getClassMetadata($class); + $this->_updateRow($parentTable->getTableName(), $dataSet[$class], $identifier); + } + + $record->assignIdentifier(true); + + return true; + } + + /** + * Deletes an entity that is part of a Class Table Inheritance hierarchy. + * + */ + protected function _doDelete(Doctrine_Entity $record) + { + $conn = $this->_conn; + try { + $class = $this->_classMetadata; + $conn->beginInternalTransaction(); + $this->_deleteComposites($record); + + $record->state(Doctrine_Entity::STATE_TDIRTY); + + $identifier = $this->_convertFieldToColumnNames($record->identifier(), $class); + + // run deletions, starting from the class, upwards the hierarchy + $conn->delete($class->getTableName(), $identifier); + foreach ($class->getParentClasses() as $parent) { + $parentClass = $conn->getClassMetadata($parent); + $this->_deleteRow($parentClass->getTableName(), $identifier); + } + + $record->state(Doctrine_Entity::STATE_TCLEAN); + + $this->removeRecord($record); // @todo should be done in the unitofwork + $conn->commit(); + } catch (Exception $e) { + $conn->rollback(); + throw $e; + } + + return true; + } + + /** + * Adds all parent classes as INNER JOINs and subclasses as OUTER JOINs + * to the query. + * + * Callback that is invoked during the SQL construction process. + * + * @return array The custom joins in the format => + */ + public function getCustomJoins() + { + $customJoins = array(); + $classMetadata = $this->_classMetadata; + foreach ($classMetadata->getParentClasses() as $parentClass) { + $customJoins[$parentClass] = 'INNER'; + } + foreach ($classMetadata->getSubclasses() as $subClass) { + if ($subClass != $this->getComponentName()) { + $customJoins[$subClass] = 'LEFT'; + } + } + + return $customJoins; + } + + /** + * Adds the discriminator column to the selected fields in a query as well as + * all fields of subclasses. In Class Table Inheritance the default behavior is that + * all subclasses are joined in through OUTER JOINs when querying a base class. + * + * Callback that is invoked during the SQL construction process. + * + * @return array An array with the field names that will get added to the query. + */ + public function getCustomFields() + { + $classMetadata = $this->_classMetadata; + $conn = $this->_conn; + $fields = array($classMetadata->getInheritanceOption('discriminatorColumn')); + if ($classMetadata->getSubclasses()) { + foreach ($classMetadata->getSubclasses() as $subClass) { + $fields = array_merge($conn->getClassMetadata($subClass)->getFieldNames(), $fields); + } + } + + return array_unique($fields); + } + + /** + * + */ + public function getFieldNames() + { + if ($this->_fieldNames) { + return $this->_fieldNames; + } + + $fieldNames = $this->_classMetadata->getFieldNames(); + $this->_fieldNames = array_unique($fieldNames); + + return $fieldNames; + } + + /** + * + */ + public function getFieldName($columnName) + { + if (isset($this->_columnNameFieldNameMap[$columnName])) { + return $this->_columnNameFieldNameMap[$columnName]; + } + + $classMetadata = $this->_classMetadata; + $conn = $this->_conn; + + if ($classMetadata->hasColumn($columnName)) { + $this->_columnNameFieldNameMap[$columnName] = $classMetadata->getFieldName($columnName); + return $this->_columnNameFieldNameMap[$columnName]; + } + + foreach ($classMetadata->getSubclasses() as $subClass) { + $subTable = $conn->getClassMetadata($subClass); + if ($subTable->hasColumn($columnName)) { + $this->_columnNameFieldNameMap[$columnName] = $subTable->getFieldName($columnName); + return $this->_columnNameFieldNameMap[$columnName]; + } + } + + throw new Doctrine_Mapper_Exception("No field name found for column name '$columnName'."); + } + + /** + * + * @todo Looks like this better belongs into the ClassMetadata class. + */ + public function getOwningClass($fieldName) + { + $conn = $this->_conn; + $classMetadata = $this->_classMetadata; + if ($classMetadata->hasField($fieldName) && ! $classMetadata->isInheritedField($fieldName)) { + return $classMetadata; + } + + foreach ($classMetadata->getParentClasses() as $parentClass) { + $parentTable = $conn->getClassMetadata($parentClass); + if ($parentTable->hasField($fieldName) && ! $parentTable->isInheritedField($fieldName)) { + return $parentTable; + } + } + + foreach ((array)$classMetadata->getSubclasses() as $subClass) { + $subTable = $conn->getClassMetadata($subClass); + if ($subTable->hasField($fieldName) && ! $subTable->isInheritedField($fieldName)) { + return $subTable; + } + } + + throw new Doctrine_Mapper_Exception("Unable to find defining class of field '$fieldName'."); + } + + /** + * Analyzes the fields of the entity and creates a map in which the field names + * are grouped by the class names they belong to. + * + * @return array + */ + protected function _groupFieldsByDefiningClass(Doctrine_Entity $record) + { + $conn = $this->_conn; + $classMetadata = $this->_classMetadata; + $dataSet = array(); + $component = $classMetadata->getClassName(); + $array = $record->getPrepared(); + + $classes = array_merge(array($component), $classMetadata->getParentClasses()); + + foreach ($classes as $class) { + $dataSet[$class] = array(); + $parentClassMetadata = $conn->getClassMetadata($class); + foreach ($parentClassMetadata->getColumns() as $columnName => $definition) { + if ((isset($definition['primary']) && $definition['primary'] === true) || + (isset($definition['inherited']) && $definition['inherited'] === true)) { + continue; + } + $fieldName = $classMetadata->getFieldName($columnName); + if ( ! array_key_exists($fieldName, $array)) { + continue; + } + $dataSet[$class][$columnName] = $array[$fieldName]; + } + } + + return $dataSet; + } +} + diff --git a/lib/Doctrine/EntityPersister/Standard.php b/lib/Doctrine/EntityPersister/Standard.php new file mode 100644 index 000000000..8c5076960 --- /dev/null +++ b/lib/Doctrine/EntityPersister/Standard.php @@ -0,0 +1,120 @@ +. + */ + +/** + * The default mapping strategy maps a single entity instance to a single database table, + * as is the case in Single Table Inheritance & Concrete Table Inheritance. + * + * @author Roman Borschel + * @package Doctrine + * @subpackage Abstract + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @version $Revision$ + * @link www.phpdoctrine.org + * @since 2.0 + */ +class Doctrine_EntityPersister_Standard extends Doctrine_EntityPersister_Abstract +{ + /** + * Deletes an entity. + */ + protected function _doDelete(Doctrine_Entity $record) + { + $conn = $this->_conn; + $metadata = $this->_classMetadata; + try { + $conn->beginInternalTransaction(); + $this->_deleteComposites($record); + + $record->state(Doctrine_Entity::STATE_TDIRTY); + + $identifier = $this->_convertFieldToColumnNames($record->identifier(), $metadata); + $this->_deleteRow($metadata->getTableName(), $identifier); + $record->state(Doctrine_Entity::STATE_TCLEAN); + + $this->removeRecord($record); + $conn->commit(); + } catch (Exception $e) { + $conn->rollback(); + throw $e; + } + } + + /** + * Inserts a single entity into the database, without any related entities. + * + * @param Doctrine_Entity $record The entity to insert. + */ + protected function _doInsert(Doctrine_Entity $record) + { + $conn = $this->_conn; + + $fields = $record->getPrepared(); + if (empty($fields)) { + return false; + } + + //$class = $record->getClassMetadata(); + $class = $this->_classMetadata; + $identifier = $class->getIdentifier(); + $fields = $this->_convertFieldToColumnNames($fields, $class); + + $seq = $class->getTableOption('sequenceName'); + if ( ! empty($seq)) { + $id = $conn->sequence->nextId($seq); + $seqName = $identifier[0]; + $fields[$seqName] = $id; + $record->assignIdentifier($id); + } + + $this->_insertRow($class->getTableName(), $fields); + + if (empty($seq) && count($identifier) == 1 && + $class->getIdentifierType() != Doctrine::IDENTIFIER_NATURAL) { + if (strtolower($conn->getName()) == 'pgsql') { + $seq = $class->getTableName() . '_' . $identifier[0]; + } + + $id = $conn->sequence->lastInsertId($seq); + + if ( ! $id) { + throw new Doctrine_Mapper_Exception("Couldn't get last insert identifier."); + } + + $record->assignIdentifier($id); + } else { + $record->assignIdentifier(true); + } + } + + /** + * Updates an entity. + */ + protected function _doUpdate(Doctrine_Entity $record) + { + $conn = $this->_conn; + $classMetadata = $this->_classMetadata; + $identifier = $this->_convertFieldToColumnNames($record->identifier(), $classMetadata); + $data = $this->_convertFieldToColumnNames($record->getPrepared(), $classMetadata); + $this->_updateRow($classMetadata->getTableName(), $data, $identifier); + $record->assignIdentifier(true); + } +} \ No newline at end of file diff --git a/lib/Doctrine/HydratorNew.php b/lib/Doctrine/HydratorNew.php index ee31f4b81..b31de007e 100644 --- a/lib/Doctrine/HydratorNew.php +++ b/lib/Doctrine/HydratorNew.php @@ -24,7 +24,8 @@ * and turn them into useable structures. * * Runtime complexity: The following gives the overall number of iterations - * required to process a result set. + * required to process a result set when using identity hydration + * (HYDRATE_IDENTITY_OBJECT or HYDRATE_IDENTITY_ARRAY). * * numRowsInResult * numColumnsInResult + numRowsInResult * numClassesInQuery * @@ -32,15 +33,19 @@ * * (numRowsInResult * (numColumnsInResult + numClassesInQuery)) * - * Note that this is only a crude definition of the complexity as it also heavily + * For scalar hydration (HYDRATE_SCALAR) it's: + * + * numRowsInResult * numColumnsInResult + * + * Note that this is only a crude definition as it also heavily * depends on the complexity of all the single operations that are performed in * each iteration. * * As can be seen, the number of columns in the result has the most impact on - * the overall performance (apart from the row counr, of course), since numClassesInQuery + * the overall performance (apart from the row count, of course), since numClassesInQuery * is usually pretty low. - * That's why the performance of the gatherRowData() method which is responsible - * for the "numRowsInResult * numColumnsInResult" part is crucial to fast hydraton. + * That's why the performance of the _gatherRowData() methods which are responsible + * for the "numRowsInResult * numColumnsInResult" part is crucial to fast hydration. * * @package Doctrine * @subpackage Hydrator @@ -54,14 +59,10 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract { /** - * hydrateResultSet - * parses the data returned by statement object + * Parses the data returned by statement object. * * This is method defines the core of Doctrine's object population algorithm. * - * The key idea is the loop over the rowset only once doing all the needed operations - * within this massive loop. - * * @todo: Detailed documentation. Refactor (too long & nesting level). * * @param mixed $stmt @@ -125,7 +126,7 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract $idTemplate = array(); // Holds the resulting hydrated data structure - if ($parserResult->isMixedQuery()) { + if ($parserResult->isMixedQuery() || $hydrationMode == Doctrine::HYDRATE_SCALAR) { $result = array(); } else { $result = $driver->getElementCollection($rootComponentName); @@ -140,7 +141,7 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract // disable lazy-loading of related elements during hydration $component['table']->setAttribute(Doctrine::ATTR_LOAD_REFERENCES, false); $componentName = $component['table']->getClassName(); - $listeners[$componentName] = $component['table']->getRecordListener(); + //$listeners[$componentName] = $component['table']->getRecordListener(); $identifierMap[$dqlAlias] = array(); $resultPointers[$dqlAlias] = array(); $idTemplate[$dqlAlias] = ''; @@ -148,7 +149,12 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract // Process result set $cache = array(); - while ($data = $stmt->fetch(Doctrine::FETCH_ASSOC)) { + while ($data = $stmt->fetch(Doctrine::FETCH_ASSOC)) { + if ($hydrationMode == Doctrine::HYDRATE_SCALAR) { + $result[] = $this->_gatherScalarRowData($data, $cache); + continue; + } + $id = $idTemplate; // initialize the id-memory $nonemptyComponents = array(); $rowData = $this->_gatherRowData($data, $cache, $id, $nonemptyComponents); @@ -160,8 +166,8 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract $componentName = $class->getComponentName(); // just event stuff - $event->set('data', $rowData[$rootAlias]); - $listeners[$componentName]->preHydrate($event); + //$event->set('data', $rowData[$rootAlias]); + //$listeners[$componentName]->preHydrate($event); //-- // Check for an existing element @@ -170,8 +176,8 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract $element = $driver->getElement($rowData[$rootAlias], $componentName); // just event stuff - $event->set('data', $element); - $listeners[$componentName]->postHydrate($event); + //$event->set('data', $element); + //$listeners[$componentName]->postHydrate($event); //-- // do we need to index by a custom field? @@ -201,7 +207,6 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract } else { $index = $identifierMap[$rootAlias][$id[$rootAlias]]; } - $this->_setLastElement($resultPointers, $result, $index, $rootAlias, false); unset($rowData[$rootAlias]); // end hydrate data of the root component for the current row @@ -212,7 +217,6 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract unset($rowData['scalars']); } - // $resultPointers[$rootAlias] now points to the last element in $result. // now hydrate the rest of the data found in the current row, that belongs to other // (related) components. foreach ($rowData as $dqlAlias => $data) { @@ -221,14 +225,13 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract $componentName = $map['table']->getComponentName(); // just event stuff - $event->set('data', $data); - $listeners[$componentName]->preHydrate($event); + //$event->set('data', $data); + //$listeners[$componentName]->preHydrate($event); //-- $parent = $map['parent']; $relation = $map['relation']; $relationAlias = $relation->getAlias(); - $path = $parent . '.' . $dqlAlias; // pick the right element that will get the associated element attached @@ -252,8 +255,8 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract $element = $driver->getElement($data, $componentName); // just event stuff - $event->set('data', $element); - $listeners[$componentName]->postHydrate($event); + //$event->set('data', $element); + //$listeners[$componentName]->postHydrate($event); //-- if ($field = $this->_getCustomIndexField($dqlAlias)) { @@ -268,7 +271,6 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract } else { $driver->addRelatedElement($baseElement, $relationAlias, $element); } - $identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = $driver->getLastKey( $driver->getReferenceValue($baseElement, $relationAlias)); } else { @@ -361,9 +363,12 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract } /** - * Puts the fields of a data row into a new array, grouped by the component + * 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. + * 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). @@ -377,7 +382,7 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract if ( ! isset($cache[$key])) { // check ignored names. fastest solution for now. if we get more we'll start // to introduce a list. - if ($key == 'doctrine_rownum') continue; + if ($this->_isIgnoredName($key)) continue; // cache general information like the column name <-> field name mapping $e = explode('__', $key); @@ -441,18 +446,94 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract 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. + */ + 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])) { + // check ignored names. fastest solution for now. if we get more we'll start + // to introduce a list. + if ($this->_isIgnoredName($key)) continue; + + // cache general information like the column name <-> field name mapping + $e = explode('__', $key); + $columnName = strtolower(array_pop($e)); + $cache[$key]['dqlAlias'] = $this->_tableAliases[strtolower(implode('__', $e))]; + $mapper = $this->_queryComponents[$cache[$key]['dqlAlias']]['mapper']; + $classMetadata = $mapper->getClassMetadata(); + // 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 = $mapper->getFieldName($columnName); + $cache[$key]['isScalar'] = false; + } + + $cache[$key]['fieldName'] = $fieldName; + + // cache type information + $type = $classMetadata->getTypeOfColumn($columnName); + if ($type == 'integer' || $type == 'string') { + $cache[$key]['isSimpleType'] = true; + } else { + $cache[$key]['type'] = $type; + $cache[$key]['isSimpleType'] = false; + } + } + + $mapper = $this->_queryComponents[$cache[$key]['dqlAlias']]['mapper']; + $dqlAlias = $cache[$key]['dqlAlias']; + $fieldName = $cache[$key]['fieldName']; + + if ($cache[$key]['isSimpleType'] || $cache[$key]['isScalar']) { + $rowData[$dqlAlias . '_' . $fieldName] = $value; + } else { + $rowData[$dqlAlias . '_' . $fieldName] = $mapper->prepareValue( + $fieldName, $value, $cache[$key]['type']); + } + } + + 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. */ - protected function _getCustomIndexField($alias) + 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'; + } + /** Needed only temporarily until the new parser is ready */ private $_isResultMixed = false; public function setResultMixed($bool) { diff --git a/tests/Orm/Hydration/BasicHydrationTest.php b/tests/Orm/Hydration/BasicHydrationTest.php index 2b8203171..3cba5a78e 100644 --- a/tests/Orm/Hydration/BasicHydrationTest.php +++ b/tests/Orm/Hydration/BasicHydrationTest.php @@ -17,7 +17,8 @@ class Orm_Hydration_BasicHydrationTest extends Doctrine_OrmTestCase { return array( array('hydrationMode' => Doctrine::HYDRATE_RECORD), - array('hydrationMode' => Doctrine::HYDRATE_ARRAY) + array('hydrationMode' => Doctrine::HYDRATE_ARRAY), + array('hydrationMode' => Doctrine::HYDRATE_SCALAR) ); } @@ -74,20 +75,29 @@ class Orm_Hydration_BasicHydrationTest extends Doctrine_OrmTestCase $hydrator = new Doctrine_HydratorNew($this->_em); $result = $hydrator->hydrateResultSet($this->_createParserResult( - $stmt, $queryComponents, $tableAliasMap, $hydrationMode)); + $stmt, $queryComponents, $tableAliasMap, $hydrationMode)); - $this->assertEquals(2, count($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']); + if ($hydrationMode == Doctrine::HYDRATE_ARRAY || $hydrationMode == Doctrine::HYDRATE_RECORD) { + $this->assertEquals(2, count($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']); + } if ($hydrationMode == Doctrine::HYDRATE_RECORD) { $this->assertTrue($result instanceof Doctrine_Collection); $this->assertTrue($result[0] instanceof Doctrine_Entity); $this->assertTrue($result[1] instanceof Doctrine_Entity); - } else { + } else if ($hydrationMode == Doctrine::HYDRATE_ARRAY) { $this->assertTrue(is_array($result)); + } else if ($hydrationMode == Doctrine::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']); } } @@ -160,21 +170,23 @@ class Orm_Hydration_BasicHydrationTest extends Doctrine_OrmTestCase //var_dump($result); } - $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']); + if ($hydrationMode == Doctrine::HYDRATE_ARRAY || $hydrationMode == Doctrine::HYDRATE_RECORD) { + $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']); + } if ($hydrationMode == Doctrine::HYDRATE_RECORD) { $this->assertTrue($result[0][0] instanceof Doctrine_Entity); @@ -183,7 +195,17 @@ class Orm_Hydration_BasicHydrationTest extends Doctrine_OrmTestCase $this->assertTrue($result[0][0]['phonenumbers'][1] instanceof Doctrine_Entity); $this->assertTrue($result[1][0] instanceof Doctrine_Entity); $this->assertTrue($result[1][0]['phonenumbers'] instanceof Doctrine_Collection); - } + } else if ($hydrationMode == Doctrine::HYDRATE_SCALAR) { + $this->assertTrue(is_array($result)); + $this->assertEquals(3, count($result)); + + $this->assertEquals(1, $result[0]['u_id']); + $this->assertEquals('developer', $result[0]['u_status']); + $this->assertEquals('ROMANB', $result[0]['u_nameUpper']); + $this->assertEquals(42, $result[0]['p_phonenumber']); + + // ... more checks to come + } } /** @@ -236,7 +258,6 @@ class Orm_Hydration_BasicHydrationTest extends Doctrine_OrmTestCase 'p__0' => '1', ) ); - $stmt = new Doctrine_HydratorMockStatement($resultSet); $hydrator = new Doctrine_HydratorNew($this->_em); @@ -245,19 +266,31 @@ class Orm_Hydration_BasicHydrationTest extends Doctrine_OrmTestCase $stmt, $queryComponents, $tableAliasMap, $hydrationMode, true)); //var_dump($result); - $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']); + if ($hydrationMode == Doctrine::HYDRATE_ARRAY || $hydrationMode == Doctrine::HYDRATE_RECORD) { + $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']); + } if ($hydrationMode == Doctrine::HYDRATE_RECORD) { $this->assertTrue($result[0][0] instanceof Doctrine_Entity); $this->assertTrue($result[1][0] instanceof Doctrine_Entity); + } else if ($hydrationMode == Doctrine::HYDRATE_SCALAR) { + $this->assertEquals(2, count($result)); + + $this->assertEquals(1, $result[0]['u_id']); + $this->assertEquals('developer', $result[0]['u_status']); + $this->assertEquals(2, $result[0]['p_numPhones']); + + $this->assertEquals(2, $result[1]['u_id']); + $this->assertEquals('developer', $result[1]['u_status']); + $this->assertEquals(1, $result[1]['p_numPhones']); } } @@ -330,31 +363,35 @@ class Orm_Hydration_BasicHydrationTest extends Doctrine_OrmTestCase //var_dump($result); } - $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. 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'])); - - // test the scalar values - $this->assertEquals('ROMANB', $result[0]['nameUpper']); - $this->assertEquals('JWAGE', $result[1]['nameUpper']); - + if ($hydrationMode == Doctrine::HYDRATE_ARRAY || $hydrationMode == Doctrine::HYDRATE_RECORD) { + $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. 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'])); + + // test the scalar values + $this->assertEquals('ROMANB', $result[0]['nameUpper']); + $this->assertEquals('JWAGE', $result[1]['nameUpper']); + } + if ($hydrationMode == Doctrine::HYDRATE_RECORD) { $this->assertTrue($result[0]['1'] instanceof Doctrine_Entity); $this->assertTrue($result[1]['2'] instanceof Doctrine_Entity); $this->assertTrue($result[0]['1']['phonenumbers'] instanceof Doctrine_Collection); $this->assertEquals(2, count($result[0]['1']['phonenumbers'])); + } else if ($hydrationMode == Doctrine::HYDRATE_SCALAR) { + // NOTE: Indexing has no effect with HYDRATE_SCALAR + //... asserts to come } } @@ -469,28 +506,30 @@ class Orm_Hydration_BasicHydrationTest extends Doctrine_OrmTestCase //var_dump($result); } - $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']); + if ($hydrationMode == Doctrine::HYDRATE_ARRAY || $hydrationMode == Doctrine::HYDRATE_RECORD) { + $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']); + } if ($hydrationMode == Doctrine::HYDRATE_RECORD) { $this->assertTrue($result[0][0] instanceof Doctrine_Entity); @@ -505,6 +544,10 @@ class Orm_Hydration_BasicHydrationTest extends Doctrine_OrmTestCase $this->assertTrue($result[1][0]['phonenumbers'][0] instanceof Doctrine_Entity); $this->assertTrue($result[1][0]['articles'][0] instanceof Doctrine_Entity); $this->assertTrue($result[1][0]['articles'][1] instanceof Doctrine_Entity); + } else if ($hydrationMode == Doctrine::HYDRATE_SCALAR) { + //... + $this->assertEquals(6, count($result)); + //var_dump($result); } } @@ -642,37 +685,39 @@ class Orm_Hydration_BasicHydrationTest extends Doctrine_OrmTestCase //var_dump($result); } - $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']); + if ($hydrationMode == Doctrine::HYDRATE_ARRAY || $hydrationMode == Doctrine::HYDRATE_RECORD) { + $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'])); + $this->assertFalse(isset($result[0][0]['articles'][1]['comments'])); + $this->assertFalse(isset($result[1][0]['articles'][0]['comments'])); + $this->assertFalse(isset($result[1][0]['articles'][1]['comments'])); + } - $this->assertTrue(isset($result[0][0]['articles'][0]['comments'])); - $this->assertFalse(isset($result[0][0]['articles'][1]['comments'])); - $this->assertFalse(isset($result[1][0]['articles'][0]['comments'])); - $this->assertFalse(isset($result[1][0]['articles'][1]['comments'])); - if ($hydrationMode == Doctrine::HYDRATE_RECORD) { $this->assertTrue($result[0][0] instanceof Doctrine_Entity); $this->assertTrue($result[1][0] instanceof Doctrine_Entity); @@ -691,6 +736,8 @@ class Orm_Hydration_BasicHydrationTest extends Doctrine_OrmTestCase // article comments $this->assertTrue($result[0][0]['articles'][0]['comments'] instanceof Doctrine_Collection); $this->assertTrue($result[0][0]['articles'][0]['comments'][0] instanceof Doctrine_Entity); + } else if ($hydrationMode == Doctrine::HYDRATE_SCALAR) { + //... } } @@ -787,21 +834,28 @@ class Orm_Hydration_BasicHydrationTest extends Doctrine_OrmTestCase //var_dump($result); } - $this->assertEquals(2, count($result)); - $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'])); + if ($hydrationMode == Doctrine::HYDRATE_ARRAY || $hydrationMode == Doctrine::HYDRATE_RECORD) { + $this->assertEquals(2, count($result)); + $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'])); + } if ($hydrationMode == Doctrine::HYDRATE_ARRAY) { $this->assertTrue(is_array($result)); $this->assertTrue(is_array($result[0])); $this->assertTrue(is_array($result[1])); - } else { + } else if ($hydrationMode == Doctrine::HYDRATE_RECORD) { $this->assertTrue($result instanceof Doctrine_Collection); $this->assertTrue($result[0] instanceof Doctrine_Entity); $this->assertTrue($result[1] instanceof Doctrine_Entity); - } + } else if ($hydrationMode == Doctrine::HYDRATE_SCALAR) { + //... + } } + + + }