diff --git a/lib/Doctrine/Common/Collections/Collection.php b/lib/Doctrine/Common/Collections/Collection.php index 567870e9f..e819bd577 100644 --- a/lib/Doctrine/Common/Collections/Collection.php +++ b/lib/Doctrine/Common/Collections/Collection.php @@ -392,4 +392,4 @@ class Collection implements Countable, IteratorAggregate, ArrayAccess { $this->_elements = array(); } -} \ No newline at end of file +} diff --git a/lib/Doctrine/Common/Util/Debug.php b/lib/Doctrine/Common/Util/Debug.php new file mode 100644 index 000000000..32ba0cecb --- /dev/null +++ b/lib/Doctrine/Common/Util/Debug.php @@ -0,0 +1,52 @@ +. + */ + +namespace Doctrine\Common\Util; + +/** + * Static class containing most used debug methods. + * @author Giorgio Sironi + * @since 2.0 + */ +final class Debug +{ + private function __construct() {} + + /** + * Prints a dump of the public, protected and private properties of $var. + * To print a meaningful dump, whose depth is limited, requires xdebug + * php extension. + * @link http://xdebug.org/ + * @param mixed $var + * @param integer $maxDepth maximum nesting level for object properties + */ + public static function dump($var, $maxDepth = 2) + { + ini_set('html_errors', 'On'); + ini_set('xdebug.var_display_max_depth', $maxDepth); + ob_start(); + var_dump($var); + $dump = ob_get_contents(); + ob_end_clean(); + echo strip_tags(html_entity_decode($dump)); + ini_set('html_errors', 'Off'); + } +} diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 7dd9aa086..61e684a55 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -137,7 +137,6 @@ class ObjectHydrator extends AbstractHydrator * 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 @@ -173,6 +172,7 @@ class ObjectHydrator extends AbstractHydrator * * @param object $entity The entity to which the collection belongs. * @param string $name The name of the field on the entity that holds the collection. + * @return PersistentCollection */ private function initRelatedCollection($entity, $name) { @@ -190,6 +190,7 @@ class ObjectHydrator extends AbstractHydrator $classMetadata->reflFields[$name]->setValue($entity, $coll); $this->_uow->setOriginalEntityProperty($oid, $name, $coll); $this->_initializedRelations[$oid][$name] = true; + return $coll; } /** @@ -263,13 +264,14 @@ class ObjectHydrator extends AbstractHydrator $assoc->load($entity, new $assoc->targetEntityName, $this->_em, $joinColumns); } } else { - //TODO: Eager load // Inject collection - $this->_ce[$className]->reflFields[$field] - ->setValue($entity, new PersistentCollection( - $this->_em, - $this->_em->getClassMetadata($assoc->targetEntityName) - )); + $collection = $this->initRelatedCollection($entity, $field); + if ($assoc->isLazilyFetched()) { + $collection->setLazyInitialization(); + } else { + //TODO: Allow more efficient and configurable batching of these loads + $assoc->load($entity, $collection, $this->_em); + } } } } diff --git a/lib/Doctrine/ORM/Mapping/AssociationMapping.php b/lib/Doctrine/ORM/Mapping/AssociationMapping.php index 904106a20..3aedb7857 100644 --- a/lib/Doctrine/ORM/Mapping/AssociationMapping.php +++ b/lib/Doctrine/ORM/Mapping/AssociationMapping.php @@ -391,14 +391,28 @@ abstract class AssociationMapping } /** - * Loads data in $targetEntity domain object using this association. + * Loads data in $target domain object using this association. * The data comes from the association navigated from $sourceEntity * using $em. * * @param object $sourceEntity - * @param object $targetEntity + * @param object $target an entity or a collection * @param EntityManager $em - * @param array $joinColumnValues + * @param array $joinColumnValues foreign keys (significative for this + * association) of $sourceEntity, if needed */ - abstract public function load($sourceEntity, $targetEntity, $em, array $joinColumnValues); + abstract public function load($sourceEntity, $target, $em, array $joinColumnValues = array()); + + /** + * Uses reflection to access internal data of entities. + * @param ClassMetadata $class + * @param $entity a domain object + * @param $column name of private field + * @return mixed + */ + protected function _getPrivateValue(ClassMetadata $class, $entity, $column) + { + $reflField = $class->getReflectionProperty($class->getFieldName($column)); + return $reflField->getValue($entity); + } } diff --git a/lib/Doctrine/ORM/Mapping/ManyToManyMapping.php b/lib/Doctrine/ORM/Mapping/ManyToManyMapping.php index 5a91e3ef1..f32abc675 100644 --- a/lib/Doctrine/ORM/Mapping/ManyToManyMapping.php +++ b/lib/Doctrine/ORM/Mapping/ManyToManyMapping.php @@ -35,6 +35,7 @@ namespace Doctrine\ORM\Mapping; * * @since 2.0 * @author Roman Borschel + * @author Giorgio Sironi */ class ManyToManyMapping extends AssociationMapping { @@ -138,9 +139,47 @@ class ManyToManyMapping extends AssociationMapping return $this->targetKeyColumns; } - public function load($owningEntity, $targetEntity, $em, array $joinColumnValues) + /** + * Loads entities in $targetCollection using $em. + * The data of $sourceEntity are used to restrict the collection + * via the join table. + */ + public function load($sourceEntity, $targetCollection, $em, array $joinColumnValues = array()) { - throw new Exception('Not yet implemented.'); + $sourceClass = $em->getClassMetadata($this->sourceEntityName); + $joinTableConditions = array(); + if ($this->isOwningSide()) { + $joinTable = $this->getJoinTable(); + $joinClauses = $this->getTargetToRelationKeyColumns(); + foreach ($this->getSourceToRelationKeyColumns() as $sourceKeyColumn => $relationKeyColumn) { + // getting id + if (isset($sourceClass->reflFields[$sourceKeyColumn])) { + $joinTableConditions[$relationKeyColumn] = $this->_getPrivateValue($sourceClass, $sourceEntity, $sourceKeyColumn); + } else { + $joinTableConditions[$relationKeyColumn] = $joinColumnValues[$sourceKeyColumn]; + } + } + } else { + $owningAssoc = $em->getClassMetadata($this->targetEntityName)->getAssociationMapping($this->mappedByFieldName); + $joinTable = $owningAssoc->getJoinTable(); + // TRICKY: since the association is inverted source and target are flipped + $joinClauses = $owningAssoc->getSourceToRelationKeyColumns(); + foreach ($owningAssoc->getTargetToRelationKeyColumns() as $sourceKeyColumn => $relationKeyColumn) { + // getting id + if (isset($sourceClass->reflFields[$sourceKeyColumn])) { + $joinTableConditions[$relationKeyColumn] = $this->_getPrivateValue($sourceClass, $sourceEntity, $sourceKeyColumn); + } else { + $joinTableConditions[$relationKeyColumn] = $joinColumnValues[$sourceKeyColumn]; + } + } + } + $joinTableCriteria = array( + 'table' => $joinTable['name'], + 'join' => $joinClauses, + 'criteria' => $joinTableConditions + ); + $persister = $em->getUnitOfWork()->getEntityPersister($this->targetEntityName); + $persister->loadCollection(array($joinTableCriteria), $targetCollection); } /** diff --git a/lib/Doctrine/ORM/Mapping/OneToManyMapping.php b/lib/Doctrine/ORM/Mapping/OneToManyMapping.php index d50877c6c..34d30177e 100644 --- a/lib/Doctrine/ORM/Mapping/OneToManyMapping.php +++ b/lib/Doctrine/ORM/Mapping/OneToManyMapping.php @@ -38,6 +38,7 @@ namespace Doctrine\ORM\Mapping; * the serialized representation). * * @author Roman Borschel + * @author Giorgio Sironi * @since 2.0 */ class OneToManyMapping extends AssociationMapping @@ -47,6 +48,13 @@ class OneToManyMapping extends AssociationMapping /** FUTURE: The key column mapping, if any. The key column holds the keys of the Collection. */ //public $keyColumn; + /** + * TODO: Allow any combination of source/target columns in lazy loading. + * What is supported now is primary key (that can spread on multiple fields) + * pointed to foreign keys on the target + public $targetColumns; + */ + /** * Initializes a new OneToManyMapping. * @@ -67,7 +75,7 @@ class OneToManyMapping extends AssociationMapping protected function _validateAndCompleteMapping(array $mapping) { parent::_validateAndCompleteMapping($mapping); - + // one-side MUST be inverse (must have mappedBy) if ( ! isset($mapping['mappedBy'])) { throw MappingException::oneToManyRequiresMappedBy($mapping['fieldName']); @@ -97,8 +105,22 @@ class OneToManyMapping extends AssociationMapping return true; } - public function load($owningEntity, $targetEntity, $em, array $joinColumnValues) + public function load($sourceEntity, $targetCollection, $em, array $joinColumnValues = array()) { - throw new Exception('Not yet implemented.'); + $persister = $em->getUnitOfWork()->getEntityPersister($this->targetEntityName); + // a one-to-many is always inverse (does not have foreign key) + $sourceClass = $em->getClassMetadata($this->sourceEntityName); + $owningAssoc = $em->getClassMetadata($this->targetEntityName)->getAssociationMapping($this->mappedByFieldName); + // TRICKY: since the association is specular source and target are flipped + foreach ($owningAssoc->getTargetToSourceKeyColumns() as $sourceKeyColumn => $targetKeyColumn) { + // getting id + if (isset($sourceClass->reflFields[$sourceKeyColumn])) { + $conditions[$targetKeyColumn] = $this->_getPrivateValue($sourceClass, $sourceEntity, $sourceKeyColumn); + } else { + $conditions[$targetKeyColumn] = $joinColumnValues[$sourceKeyColumn]; + } + } + + $persister->loadCollection($conditions, $targetCollection); } } diff --git a/lib/Doctrine/ORM/Mapping/OneToOneMapping.php b/lib/Doctrine/ORM/Mapping/OneToOneMapping.php index 592a284e5..7d6e83768 100644 --- a/lib/Doctrine/ORM/Mapping/OneToOneMapping.php +++ b/lib/Doctrine/ORM/Mapping/OneToOneMapping.php @@ -163,7 +163,7 @@ class OneToOneMapping extends AssociationMapping * @param array $joinColumnValues values of the join columns of $sourceEntity. There are no fields * to store this data in $sourceEntity */ - public function load($sourceEntity, $targetEntity, $em, array $joinColumnValues) + public function load($sourceEntity, $targetEntity, $em, array $joinColumnValues = array()) { $sourceClass = $em->getClassMetadata($this->sourceEntityName); $targetClass = $em->getClassMetadata($this->targetEntityName); @@ -189,12 +189,13 @@ class OneToOneMapping extends AssociationMapping } } else { $owningAssoc = $em->getClassMetadata($this->targetEntityName)->getAssociationMapping($this->mappedByFieldName); - foreach ($owningAssoc->getTargetToSourceKeyColumns() as $targetKeyColumn => $sourceKeyColumn) { + // TRICKY: since the association is specular source and target are flipped + foreach ($owningAssoc->getTargetToSourceKeyColumns() as $sourceKeyColumn => $targetKeyColumn) { // getting id - if (isset($sourceClass->reflFields[$targetKeyColumn])) { - $conditions[$sourceKeyColumn] = $this->_getPrivateValue($sourceClass, $sourceEntity, $targetKeyColumn); + if (isset($sourceClass->reflFields[$sourceKeyColumn])) { + $conditions[$targetKeyColumn] = $this->_getPrivateValue($sourceClass, $sourceEntity, $sourceKeyColumn); } else { - $conditions[$sourceKeyColumn] = $joinColumnValues[$targetKeyColumn]; + $conditions[$targetKeyColumn] = $joinColumnValues[$sourceKeyColumn]; } } @@ -203,10 +204,4 @@ class OneToOneMapping extends AssociationMapping $targetClass->setFieldValue($targetEntity, $this->mappedByFieldName, $sourceEntity); } } - - protected function _getPrivateValue(ClassMetadata $class, $entity, $column) - { - $reflField = $class->getReflectionProperty($class->getFieldName($column)); - return $reflField->getValue($entity); - } } diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index 089d609d9..1c3432666 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -23,6 +23,7 @@ namespace Doctrine\ORM; use Doctrine\Common\DoctrineException; use Doctrine\ORM\Mapping\AssociationMapping; +use \Closure; /** * A PersistentCollection represents a collection of elements that have persistent state. @@ -41,6 +42,7 @@ use Doctrine\ORM\Mapping\AssociationMapping; * @version $Revision: 4930 $ * @author Konsta Vesterinen * @author Roman Borschel + * @author Giorgio Sironi */ final class PersistentCollection extends \Doctrine\Common\Collections\Collection { @@ -110,7 +112,7 @@ final class PersistentCollection extends \Doctrine\Common\Collections\Collection private $_isDirty = false; /** Whether the collection has already been initialized. */ - private $_initialized = false; + protected $_initialized = true; /** * Creates a new persistent collection. @@ -188,74 +190,36 @@ final class PersistentCollection extends \Doctrine\Common\Collections\Collection return $this->_typeClass; } - /** - * Removes an element from the collection. - * - * @param mixed $key - * @return boolean - * @override - */ - public function remove($key) + public function setLazyInitialization() { - $removed = parent::remove($key); - if ($removed) { - $this->_changed(); - if ($this->_association->isOneToMany() && $this->_association->orphanRemoval) { - $this->_em->getUnitOfWork()->scheduleOrphanRemoval($removed); - } - } - - return $removed; - } - - /** - * When the collection is used as a Map this is like put(key,value)/add(key,value). - * When the collection is used as a List this is like add(position,value). - * - * @param integer $key - * @param mixed $value - * @override - */ - public function set($key, $value) - { - parent::set($key, $value); - $this->_changed(); - } - - /** - * Adds an element to the collection. - * - * @param mixed $value - * @param string $key - * @return boolean Always TRUE. - * @override - */ - public function add($value) - { - parent::add($value); - $this->_changed(); - return true; + $this->_initialized = false; } /** * INTERNAL: - * Adds an element to a collection during hydration. + * Adds an element to a collection during hydration, if it not already set. + * This control is performed to avoid infinite recursion during hydration + * of bidirectional many-to-many collections. * - * @param mixed $value The element to add. + * @param mixed $element The element to add. */ - public function hydrateAdd($value) + public function hydrateAdd($element) { - parent::add($value); + if (parent::contains($element)) { + return; + } + parent::add($element); if ($this->_backRefFieldName) { // Set back reference to owner if ($this->_association->isOneToMany()) { // OneToMany $this->_typeClass->reflFields[$this->_backRefFieldName] - ->setValue($value, $this->_owner); + ->setValue($element, $this->_owner); } else { // ManyToMany - $this->_typeClass->reflFields[$this->_backRefFieldName] - ->getValue($value)->add($this->_owner); + $otherCollection = $this->_typeClass->reflFields[$this->_backRefFieldName] + ->getValue($element); + $otherCollection->hydrateAdd($this->_owner); } } } @@ -272,42 +236,15 @@ final class PersistentCollection extends \Doctrine\Common\Collections\Collection parent::set($key, $value); } - /** - * Checks whether an element is contained in the collection. - * This is an O(n) operation. - */ - public function contains($element) - { - - if ( ! $this->_initialized) { - //TODO: Probably need to hit the database here...? - //return $this->_checkElementExistence($element); - } - return parent::contains($element); - } - - /** - * @override - */ - public function count() - { - if ( ! $this->_initialized) { - //TODO: Initialize - } - return parent::count(); - } - - private function _checkElementExistence($element) - { - - } - /** * Initializes the collection by loading its contents from the database. */ private function _initialize() { - + if (!$this->_initialized) { + $this->_association->load($this->_owner, $this, $this->_em); + $this->_initialized = true; + } } /** @@ -371,25 +308,7 @@ final class PersistentCollection extends \Doctrine\Common\Collections\Collection return $this->_association; } - /** - * Clears the collection. - */ - public function clear() - { - //TODO: Register collection as dirty with the UoW if necessary - //TODO: If oneToMany() && shouldDeleteOrphan() delete entities - /*if ($this->_association->isOneToMany() && $this->_association->shouldDeleteOrphans) { - foreach ($this->_data as $entity) { - $this->_em->remove($entity); - } - }*/ - parent::clear(); - if ($this->_association->isOwningSide) { - $this->_changed(); - $this->_em->getUnitOfWork()->scheduleCollectionDeletion($this); - } - } - + /** * Marks this collection as changed/dirty. */ @@ -440,6 +359,236 @@ final class PersistentCollection extends \Doctrine\Common\Collections\Collection */ public function __sleep() { - return array('_elements'); + return array('_elements', '_initialized'); } -} \ No newline at end of file + + /** + * Decorated Collection methods. + */ + public function unwrap() + { + $this->_initialize(); + return parent::unwrap(); + } + + public function first() + { + $this->_initialize(); + return parent::first(); + } + + public function last() + { + $this->_initialize(); + return parent::last(); + } + + public function key() + { + $this->_initialize(); + return parent::key(); + } + + /** + * Removes an element from the collection. + * + * @param mixed $key + * @return boolean + * @override + */ + public function remove($key) + { + $this->_initialize(); + $removed = parent::remove($key); + if ($removed) { + $this->_changed(); + if ($this->_association->isOneToMany() && $this->_association->orphanRemoval) { + $this->_em->getUnitOfWork()->scheduleOrphanRemoval($removed); + } + } + + return $removed; + } + + public function removeElement($element) + { + $this->_initialize(); + $result = parent::removeElement($element); + $this->_changed(); + return $result; + } + + public function offsetExists($offset) + { + $this->_initialize(); + return parent::offsetExists($offset); + } + + public function offsetGet($offset) + { + $this->_initialize(); + return parent::offsetGet($offset); + } + + public function offsetSet($offset, $value) + { + $this->_initialize(); + $result = parent::offsetSet($offset, $value); + $this->_changed(); + return $result; + } + + public function offsetUnset($offset) + { + $this->_initialize(); + $result = parent::offsetUnset($offset); + $this->_changed(); + return $result; + } + + public function containsKey($key) + { + $this->_initialize(); + return parent::containsKey($key); + } + + /** + * Checks whether an element is contained in the collection. + * This is an O(n) operation. + */ + public function contains($element) + { + $this->_initialize(); + return parent::contains($element); + } + + public function exists(Closure $p) + { + $this->_initialize(); + return parent::exists($p); + } + + public function search($element) + { + $this->_initialize(); + return parent::search($element); + } + + public function get($key) + { + $this->_initialize(); + return parent::get($key); + } + + public function getKeys() + { + $this->_initialize(); + return parent::getKeys(); + } + + public function getElements() + { + $this->_initialize(); + return parent::getElements(); + } + + /** + * @override + */ + public function count() + { + $this->_initialize(); + return parent::count(); + } + + /** + * When the collection is used as a Map this is like put(key,value)/add(key,value). + * When the collection is used as a List this is like add(position,value). + * + * @param integer $key + * @param mixed $value + * @override + */ + public function set($key, $value) + { + $this->_initialize(); + $result = parent::set($key, $value); + $this->_changed(); + return $result; + } + + /** + * Adds an element to the collection. + * + * @param mixed $value + * @param string $key + * @return boolean Always TRUE. + * @override + */ + public function add($value) + { + $this->_initialize(); + $result = parent::add($value); + $this->_changed(); + return $result; + } + + public function isEmpty() + { + $this->_initialize(); + return parent::isEmpty(); + } + + public function getIterator() + { + $this->_initialize(); + return parent::getIterator(); + } + + public function map(Closure $func) + { + $this->_initialize(); + $result = parent::map($func); + $this->_changed(); + return $result; + } + + public function filter(Closure $p) + { + $this->_initialize(); + return parent::filter($p); + } + + public function forAll(Closure $p) + { + $this->_initialize(); + return parent::forAll($p); + } + + public function partition(Closure $p) + { + $this->_initialize(); + return parent::partition($p); + } + + /** + * Clears the collection. + */ + public function clear() + { + $this->_initialize(); + //TODO: Register collection as dirty with the UoW if necessary + //TODO: If oneToMany() && shouldDeleteOrphan() delete entities + /*if ($this->_association->isOneToMany() && $this->_association->shouldDeleteOrphans) { + foreach ($this->_data as $entity) { + $this->_em->remove($entity); + } + }*/ + $result = parent::clear(); + if ($this->_association->isOwningSide) { + $this->_changed(); + $this->_em->getUnitOfWork()->scheduleCollectionDeletion($this); + } + return $result; + } +} diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index 0e5783081..e6d4502f0 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -273,7 +273,7 @@ class JoinedSubclassPersister extends StandardEntityPersister * @todo Quote identifier. * @override */ - protected function _getSelectSingleEntitySql(array $criteria) + protected function _getSelectSingleEntitySql(array &$criteria) { $tableAliases = array(); $aliasIndex = 1; @@ -326,4 +326,4 @@ class JoinedSubclassPersister extends StandardEntityPersister return $sql . ' WHERE ' . $conditionSql; } -} \ No newline at end of file +} diff --git a/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php b/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php index bdd76fcf1..c9952475b 100644 --- a/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php @@ -35,6 +35,7 @@ use Doctrine\ORM\Events; * how to persist (and to some extent how to load) entities of a specific type. * * @author Roman Borschel + * @author Giorgio Sironi * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @version $Revision: 3406 $ * @link www.doctrine-project.org @@ -396,10 +397,26 @@ class StandardEntityPersister { $stmt = $this->_conn->prepare($this->_getSelectSingleEntitySql($criteria)); $stmt->execute(array_values($criteria)); - $data = array(); - $joinColumnValues = array(); $result = $stmt->fetch(Connection::FETCH_ASSOC); $stmt->closeCursor(); + return $this->_createEntity($result, $entity); + } + + public function loadCollection(array $criteria, PersistentCollection $collection) + { + $sql = $this->_getSelectSingleEntitySql($criteria); + $stmt = $this->_conn->prepare($sql); + $stmt->execute(array_values($criteria)); + while ($result = $stmt->fetch(Connection::FETCH_ASSOC)) { + $collection->hydrateAdd($this->_createEntity($result)); + } + $stmt->closeCursor(); + } + + private function _createEntity($result, $entity = null) + { + $data = array(); + $joinColumnValues = array(); if ($result === false) { return null; @@ -448,10 +465,15 @@ class StandardEntityPersister } } else { // Inject collection - //TODO: Eager load - $this->_class->reflFields[$field]->setValue( - $entity, new PersistentCollection($this->_em, $this->_em->getClassMetadata($assoc->targetEntityName) - )); + $coll = new PersistentCollection($this->_em, $this->_em->getClassMetadata($assoc->targetEntityName)); + $coll->setOwner($entity, $assoc); + $this->_class->reflFields[$field]->setValue($entity, $coll); + if ($assoc->isLazilyFetched()) { + $coll->setLazyInitialization(); + } else { + //TODO: Allow more efficient and configurable batching of these loads + $assoc->load($entity, $coll, $this->_em); + } } } } @@ -461,11 +483,14 @@ class StandardEntityPersister /** * Gets the SELECT SQL to select a single entity by a set of field criteria. + * No joins are used but the query can return multiple rows. * - * @param array $criteria + * @param array $criteria every element can be a value or an joinClause + * if it is a joinClause, it will be removed and correspondent nested values will be added to $criteria. + * JoinClauses are used to restrict the result returned but only columns of this entity are selected (@see _getJoinSql()). * @return string The SQL. */ - protected function _getSelectSingleEntitySql(array $criteria) + protected function _getSelectSingleEntitySql(array &$criteria) { $columnList = ''; foreach ($this->_class->columnNames as $column) { @@ -485,9 +510,23 @@ class StandardEntityPersister } } + $joinSql = ''; $conditionSql = ''; foreach ($criteria as $field => $value) { - if ($conditionSql != '') $conditionSql .= ' AND '; + if ($conditionSql != '') { + $conditionSql .= ' AND '; + } + + if (is_array($value)) { + $joinSql .= $this->_getJoinSql($value); + foreach ($value['criteria'] as $nestedField => $nestedValue) { + $columnName = "{$value['table']}.{$nestedField}"; + $conditionSql .= $this->_conn->quoteIdentifier($columnName) . ' = ?'; + $criteria[$columnName] = $nestedValue; + } + unset($criteria[$field]); + continue; + } if (isset($this->_class->columnNames[$field])) { $columnName = $this->_class->columnNames[$field]; } else if (in_array($field, $joinColumnNames)) { @@ -498,7 +537,27 @@ class StandardEntityPersister $conditionSql .= $this->_conn->quoteIdentifier($columnName) . ' = ?'; } - return 'SELECT ' . $columnList . ' FROM ' . $this->_class->getTableName() - . ' WHERE ' . $conditionSql; + return 'SELECT ' . $columnList + . ' FROM ' . $this->_class->getTableName() + . $joinSql + . ' WHERE ' . $conditionSql; + } + + /** + * Builds a LEFT JOIN sql query part using $joinClause + * @param array $joinClause keys are: + * 'table' => table to join + * 'join' => array of [sourceField => joinTableField] + * 'criteria' => array of [joinTableField => value] + * @return string + */ + protected function _getJoinSql(array $joinClause) + { + $clauses = array(); + foreach ($joinClause['join'] as $sourceField => $joinTableField) { + $clauses[] = $this->_class->getTableName() . ".{$sourceField} = " + . "{$joinClause['table']}.{$joinTableField}"; + } + return " LEFT JOIN {$joinClause['table']} ON " . implode(' AND ', $clauses); } } diff --git a/lib/Doctrine/ORM/Query/AST/OrderByItem.php b/lib/Doctrine/ORM/Query/AST/OrderByItem.php index cd68230cf..94ded8ad1 100644 --- a/lib/Doctrine/ORM/Query/AST/OrderByItem.php +++ b/lib/Doctrine/ORM/Query/AST/OrderByItem.php @@ -70,4 +70,4 @@ class OrderByItem extends Node { return $sqlWalker->walkOrderByItem($this); } -} \ No newline at end of file +} diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 63110e7ce..8bca73a75 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -1164,16 +1164,16 @@ class Parser if ($this->_lexer->isNextToken(Lexer::T_ASC)) { $this->match(Lexer::T_ASC); $item->setAsc(true); - return $item; } if ($this->_lexer->isNextToken(Lexer::T_DESC)) { $this->match(Lexer::T_DESC); + $item->setDesc(true); + return $item; } - $item->setDesc(true); - + $item->setAsc(true); return $item; } diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 9b41a9301..13c7bfae1 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -248,7 +248,11 @@ class SqlWalker implements TreeWalker $qComp = $this->_queryComponents[$dqlAlias]; $columnName = $qComp['metadata']->getColumnName($parts[0]); $sql = $this->getSqlTableAlias($qComp['metadata']->getTableName(), $dqlAlias) . '.' . $columnName; - $sql .= $orderByItem->isAsc() ? ' ASC' : ' DESC'; + if ($orderByItem->isAsc()) { + $sql .= ' ASC '; + } else if ($orderByItem->isDesc()) { + $sql .= ' DESC '; + } return $sql; } diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index aadb457c1..72018765f 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1938,4 +1938,4 @@ class UnitOfWork implements PropertyChangedListener $this->_entityUpdates[$oid] = $entity; } } -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index 5530c909f..17637fdeb 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -349,4 +349,4 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->refresh($user); $this->assertEquals('developer', $user->status); } -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php b/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php index f5186989a..4bc13c70b 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php @@ -86,5 +86,6 @@ class DetachedEntityTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertTrue($this->_em->contains($phonenumbers[0])); $this->assertTrue($this->_em->contains($phonenumbers[1])); } + } diff --git a/tests/Doctrine/Tests/ORM/Functional/ManyToManyBidirectionalAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/ManyToManyBidirectionalAssociationTest.php index 0abc7fd68..4438f9c5c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ManyToManyBidirectionalAssociationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ManyToManyBidirectionalAssociationTest.php @@ -5,12 +5,13 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Common\Collections\Collection; use Doctrine\Tests\Models\ECommerce\ECommerceProduct; use Doctrine\Tests\Models\ECommerce\ECommerceCategory; +use Doctrine\ORM\Mapping\AssociationMapping; require_once __DIR__ . '/../../TestInit.php'; /** * Tests a bidirectional many-to-many association mapping (without inheritance). - * Inverse side is not present. + * Owning side is ECommerceProduct, inverse side is ECommerceCategory. */ class ManyToManyBidirectionalAssociationTest extends AbstractManyToManyAssociationTestCase { @@ -67,36 +68,45 @@ class ManyToManyBidirectionalAssociationTest extends AbstractManyToManyAssociati $this->_createLoadingFixture(); list ($firstProduct, $secondProduct) = $this->_findProducts(); $categories = $firstProduct->getCategories(); - - $this->assertTrue($categories[0] instanceof ECommerceCategory); - $this->assertTrue($categories[1] instanceof ECommerceCategory); - $this->assertCollectionEquals($categories, $secondProduct->getCategories()); + $this->assertLoadingOfInverseSide($categories); + $this->assertLoadingOfInverseSide($secondProduct->getCategories()); } public function testEagerLoadsOwningSide() { $this->_createLoadingFixture(); - list ($firstProduct, $secondProduct) = $this->_findProducts(); - - $this->assertEquals(2, count($firstProduct->getCategories())); - $this->assertEquals(2, count($secondProduct->getCategories())); - - $categories = $firstProduct->getCategories(); - $firstCategoryProducts = $categories[0]->getProducts(); - $secondCategoryProducts = $categories[1]->getProducts(); - - $this->assertEquals(2, count($firstCategoryProducts)); - $this->assertEquals(2, count($secondCategoryProducts)); + $products = $this->_findProducts(); + $this->assertLoadingOfOwningSide($products); + } + + public function testLazyLoadsCollectionOnTheInverseSide() + { + $this->_createLoadingFixture(); - $this->assertTrue($firstCategoryProducts[0] instanceof ECommerceProduct); - $this->assertTrue($firstCategoryProducts[1] instanceof ECommerceProduct); - $this->assertTrue($secondCategoryProducts[0] instanceof ECommerceProduct); - $this->assertTrue($secondCategoryProducts[1] instanceof ECommerceProduct); - - $this->assertCollectionEquals($firstCategoryProducts, $secondCategoryProducts); + $this->_em->getConfiguration()->setAllowPartialObjects(false); + $metadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\ECommerce\ECommerceCategory'); + $metadata->getAssociationMapping('products')->fetchMode = AssociationMapping::FETCH_LAZY; + + $query = $this->_em->createQuery('SELECT c FROM Doctrine\Tests\Models\ECommerce\ECommerceCategory c order by c.id'); + $categories = $query->getResultList(); + $this->assertLoadingOfInverseSide($categories); } - protected function _createLoadingFixture() + public function testLazyLoadsCollectionOnTheOwningSide() + { + $this->_createLoadingFixture(); + + $this->_em->getConfiguration()->setAllowPartialObjects(false); + $metadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\ECommerce\ECommerceProduct'); + $metadata->getAssociationMapping('categories')->fetchMode = AssociationMapping::FETCH_LAZY; + + $query = $this->_em->createQuery('SELECT p FROM Doctrine\Tests\Models\ECommerce\ECommerceProduct p order by p.id'); + $products = $query->getResultList(); + $this->assertLoadingOfOwningSide($products); + } + + + private function _createLoadingFixture() { $this->firstProduct->addCategory($this->firstCategory); $this->firstProduct->addCategory($this->secondCategory); @@ -115,8 +125,30 @@ class ManyToManyBidirectionalAssociationTest extends AbstractManyToManyAssociati return $query->getResultList(); } - /* TODO: not yet implemented - public function testLazyLoad() { + public function assertLoadingOfOwningSide($products) + { + list ($firstProduct, $secondProduct) = $products; + $this->assertEquals(2, count($firstProduct->getCategories())); + $this->assertEquals(2, count($secondProduct->getCategories())); + + $categories = $firstProduct->getCategories(); + $firstCategoryProducts = $categories[0]->getProducts(); + $secondCategoryProducts = $categories[1]->getProducts(); - }*/ + $this->assertEquals(2, count($firstCategoryProducts)); + $this->assertEquals(2, count($secondCategoryProducts)); + + $this->assertTrue($firstCategoryProducts[0] instanceof ECommerceProduct); + $this->assertTrue($firstCategoryProducts[1] instanceof ECommerceProduct); + $this->assertTrue($secondCategoryProducts[0] instanceof ECommerceProduct); + $this->assertTrue($secondCategoryProducts[1] instanceof ECommerceProduct); + + $this->assertCollectionEquals($firstCategoryProducts, $secondCategoryProducts); + } + + public function assertLoadingOfInverseSide($categories) + { + $this->assertTrue($categories[0] instanceof ECommerceCategory); + $this->assertTrue($categories[1] instanceof ECommerceCategory); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/ManyToManySelfReferentialAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/ManyToManySelfReferentialAssociationTest.php index 01f898a97..6706a9d9a 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ManyToManySelfReferentialAssociationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ManyToManySelfReferentialAssociationTest.php @@ -4,6 +4,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Common\Collections\Collection; use Doctrine\Tests\Models\ECommerce\ECommerceProduct; +use Doctrine\ORM\Mapping\AssociationMapping; require_once __DIR__ . '/../../TestInit.php'; @@ -65,8 +66,26 @@ class ManyToManySelfReferentialAssociationTest extends AbstractManyToManyAssocia public function testEagerLoadsOwningSide() { $this->_createLoadingFixture(); - list ($firstProduct, $secondProduct) = $this->_findProducts(); - + $products = $this->_findProducts(); + $this->assertLoadingOfOwningSide($products); + } + + public function testLazyLoadsOwningSide() + { + $this->_createLoadingFixture(); + + $this->_em->getConfiguration()->setAllowPartialObjects(false); + $metadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\ECommerce\ECommerceProduct'); + $metadata->getAssociationMapping('related')->fetchMode = AssociationMapping::FETCH_LAZY; + + $query = $this->_em->createQuery('SELECT p FROM Doctrine\Tests\Models\ECommerce\ECommerceProduct p'); + $products = $query->getResultList(); + $this->assertLoadingOfOwningSide($products); + } + + public function assertLoadingOfOwningSide($products) + { + list ($firstProduct, $secondProduct) = $products; $this->assertEquals(2, count($firstProduct->getRelated())); $this->assertEquals(2, count($secondProduct->getRelated())); @@ -103,9 +122,4 @@ class ManyToManySelfReferentialAssociationTest extends AbstractManyToManyAssocia $query = $this->_em->createQuery('SELECT p, r FROM Doctrine\Tests\Models\ECommerce\ECommerceProduct p LEFT JOIN p.related r ORDER BY p.id, r.id'); return $query->getResultList(); } - - /* TODO: not yet implemented - public function testLazyLoad() { - - }*/ } diff --git a/tests/Doctrine/Tests/ORM/Functional/ManyToManyUnidirectionalAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/ManyToManyUnidirectionalAssociationTest.php index 0c050837e..f4968b0b7 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ManyToManyUnidirectionalAssociationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ManyToManyUnidirectionalAssociationTest.php @@ -5,6 +5,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Common\Collections\Collection; use Doctrine\Tests\Models\ECommerce\ECommerceCart; use Doctrine\Tests\Models\ECommerce\ECommerceProduct; +use Doctrine\ORM\Mapping\AssociationMapping; require_once __DIR__ . '/../../TestInit.php'; @@ -60,15 +61,7 @@ class ManyToManyUnidirectionalAssociationTest extends AbstractManyToManyAssociat public function testEagerLoad() { - $this->firstCart->addProduct($this->firstProduct); - $this->firstCart->addProduct($this->secondProduct); - $this->secondCart->addProduct($this->firstProduct); - $this->secondCart->addProduct($this->secondProduct); - $this->_em->persist($this->firstCart); - $this->_em->persist($this->secondCart); - - $this->_em->flush(); - $this->_em->clear(); + $this->_createFixture(); $query = $this->_em->createQuery('SELECT c, p FROM Doctrine\Tests\Models\ECommerce\ECommerceCart c LEFT JOIN c.products p ORDER BY c.id, p.id'); $result = $query->getResultList(); @@ -83,8 +76,34 @@ class ManyToManyUnidirectionalAssociationTest extends AbstractManyToManyAssociat //$this->assertEquals("Doctrine 2.x Manual", $products[1]->getName()); } - /* TODO: not yet implemented - public function testLazyLoad() { + public function testLazyLoadsCollection() + { + $this->_createFixture(); + $this->_em->getConfiguration()->setAllowPartialObjects(false); + $metadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\ECommerce\ECommerceCart'); + $metadata->getAssociationMapping('products')->fetchMode = AssociationMapping::FETCH_LAZY; + + $query = $this->_em->createQuery('SELECT c FROM Doctrine\Tests\Models\ECommerce\ECommerceCart c'); + $result = $query->getResultList(); + $firstCart = $result[0]; + $products = $firstCart->getProducts(); + $secondCart = $result[1]; - }*/ + $this->assertTrue($products[0] instanceof ECommerceProduct); + $this->assertTrue($products[1] instanceof ECommerceProduct); + $this->assertCollectionEquals($products, $secondCart->getProducts()); + } + + private function _createFixture() + { + $this->firstCart->addProduct($this->firstProduct); + $this->firstCart->addProduct($this->secondProduct); + $this->secondCart->addProduct($this->firstProduct); + $this->secondCart->addProduct($this->secondProduct); + $this->_em->persist($this->firstCart); + $this->_em->persist($this->secondCart); + + $this->_em->flush(); + $this->_em->clear(); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php index c47d56a5c..7d0e33053 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php @@ -4,6 +4,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\ECommerce\ECommerceProduct; use Doctrine\Tests\Models\ECommerce\ECommerceFeature; +use Doctrine\ORM\Mapping\AssociationMapping; require_once __DIR__ . '/../../TestInit.php'; @@ -69,13 +70,7 @@ class OneToManyBidirectionalAssociationTest extends \Doctrine\Tests\OrmFunctiona public function testEagerLoadsOneToManyAssociation() { - $this->product->addFeature($this->firstFeature); - $this->product->addFeature($this->secondFeature); - $this->_em->persist($this->product); - - $this->_em->flush(); - $this->_em->clear(); - + $this->_createFixture(); $query = $this->_em->createQuery('select p, f from Doctrine\Tests\Models\ECommerce\ECommerceProduct p join p.features f'); $result = $query->getResultList(); $product = $result[0]; @@ -89,10 +84,50 @@ class OneToManyBidirectionalAssociationTest extends \Doctrine\Tests\OrmFunctiona $this->assertEquals('Annotations examples', $features[1]->getDescription()); } - /* TODO: not yet implemented - public function testLazyLoad() { + public function testLazyLoadsObjectsOnTheOwningSide() + { + $this->_createFixture(); + $this->_em->getConfiguration()->setAllowPartialObjects(false); + $metadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\ECommerce\ECommerceProduct'); + $metadata->getAssociationMapping('features')->fetchMode = AssociationMapping::FETCH_LAZY; + + $query = $this->_em->createQuery('select p from Doctrine\Tests\Models\ECommerce\ECommerceProduct p'); + $result = $query->getResultList(); + $product = $result[0]; + $features = $product->getFeatures(); - }*/ + $this->assertTrue($features[0] instanceof ECommerceFeature); + $this->assertSame($product, $features[0]->getProduct()); + $this->assertEquals('Model writing tutorial', $features[0]->getDescription()); + $this->assertTrue($features[1] instanceof ECommerceFeature); + $this->assertSame($product, $features[1]->getProduct()); + $this->assertEquals('Annotations examples', $features[1]->getDescription()); + } + + public function testLazyLoadsObjectsOnTheInverseSide() + { + $this->_createFixture(); + $this->_em->getConfiguration()->setAllowPartialObjects(false); + $metadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\ECommerce\ECommerceFeature'); + $metadata->getAssociationMapping('product')->fetchMode = AssociationMapping::FETCH_LAZY; + + $query = $this->_em->createQuery('select f from Doctrine\Tests\Models\ECommerce\ECommerceFeature f'); + $features = $query->getResultList(); + + $product = $features[0]->getProduct(); + $this->assertTrue($product instanceof ECommerceProduct); + $this->assertSame('Doctrine Cookbook', $product->getName()); + } + + private function _createFixture() + { + $this->product->addFeature($this->firstFeature); + $this->product->addFeature($this->secondFeature); + $this->_em->persist($this->product); + + $this->_em->flush(); + $this->_em->clear(); + } public function assertFeatureForeignKeyIs($value, ECommerceFeature $feature) { $foreignKey = $this->_em->getConnection()->execute('SELECT product_id FROM ecommerce_features WHERE id=?', array($feature->getId()))->fetchColumn(); diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToManySelfReferentialAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToManySelfReferentialAssociationTest.php index bb32d3238..0b245e320 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToManySelfReferentialAssociationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToManySelfReferentialAssociationTest.php @@ -3,6 +3,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\ECommerce\ECommerceCategory; +use Doctrine\ORM\Mapping\AssociationMapping; require_once __DIR__ . '/../../TestInit.php'; @@ -69,12 +70,7 @@ class OneToManySelfReferentialAssociationTest extends \Doctrine\Tests\OrmFunctio public function testEagerLoadsOneToManyAssociation() { - $this->parent->addChild($this->firstChild); - $this->parent->addChild($this->secondChild); - $this->_em->persist($this->parent); - - $this->_em->flush(); - $this->_em->clear(); + $this->_createFixture(); $query = $this->_em->createQuery('select c1, c2 from Doctrine\Tests\Models\ECommerce\ECommerceCategory c1 join c1.children c2'); $result = $query->getResultList(); @@ -89,11 +85,36 @@ class OneToManySelfReferentialAssociationTest extends \Doctrine\Tests\OrmFunctio $this->assertSame($parent, $children[1]->getParent()); $this->assertEquals(' books', strstr($children[1]->getName(), ' books')); } - - /* TODO: not yet implemented - public function testLazyLoad() { + + public function testLazyLoadsOneToManyAssociation() + { + $this->_createFixture(); + $this->_em->getConfiguration()->setAllowPartialObjects(false); + $metadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\ECommerce\ECommerceCategory'); + $metadata->getAssociationMapping('children')->fetchMode = AssociationMapping::FETCH_LAZY; + + $query = $this->_em->createQuery('select c from Doctrine\Tests\Models\ECommerce\ECommerceCategory c order by c.id asc'); + $result = $query->getResultList(); + $parent = $result[0]; + $children = $parent->getChildren(); - }*/ + $this->assertTrue($children[0] instanceof ECommerceCategory); + $this->assertSame($parent, $children[0]->getParent()); + $this->assertEquals(' books', strstr($children[0]->getName(), ' books')); + $this->assertTrue($children[1] instanceof ECommerceCategory); + $this->assertSame($parent, $children[1]->getParent()); + $this->assertEquals(' books', strstr($children[1]->getName(), ' books')); + } + + private function _createFixture() + { + $this->parent->addChild($this->firstChild); + $this->parent->addChild($this->secondChild); + $this->_em->persist($this->parent); + + $this->_em->flush(); + $this->_em->clear(); + } public function assertForeignKeyIs($value, ECommerceCategory $child) { $foreignKey = $this->_em->getConnection()->execute('SELECT parent_id FROM ecommerce_categories WHERE id=?', array($child->getId()))->fetchColumn(); diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneSelfReferentialAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneSelfReferentialAssociationTest.php index 7ad9bf6ac..4bdf54125 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneSelfReferentialAssociationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneSelfReferentialAssociationTest.php @@ -3,6 +3,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\ECommerce\ECommerceCustomer; +use Doctrine\ORM\Mapping\AssociationMapping; require_once __DIR__ . '/../../TestInit.php'; @@ -47,7 +48,42 @@ class OneToOneSelfReferentialAssociationTest extends \Doctrine\Tests\OrmFunction $this->assertForeignKeyIs(null); } - public function testEagerLoad() + public function testEagerLoadsAssociation() + { + $this->_createFixture(); + + $query = $this->_em->createQuery('select c, m from Doctrine\Tests\Models\ECommerce\ECommerceCustomer c left join c.mentor m order by c.id asc'); + $result = $query->getResultList(); + $customer = $result[0]; + $this->assertLoadingOfAssociation($customer); + } + + public function testLazyLoadsAssociation() + { + $this->_createFixture(); + + $this->_em->getConfiguration()->setAllowPartialObjects(false); + $metadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\ECommerce\ECommerceCustomer'); + $metadata->getAssociationMapping('mentor')->fetchMode = AssociationMapping::FETCH_LAZY; + + $query = $this->_em->createQuery('select c from Doctrine\Tests\Models\ECommerce\ECommerceCustomer c'); + $result = $query->getResultList(); + $customer = $result[0]; + $this->assertLoadingOfAssociation($customer); + } + + public function assertLoadingOfAssociation($customer) + { + $this->assertTrue($customer->getMentor() instanceof ECommerceCustomer); + $this->assertEquals('Obi-wan Kenobi', $customer->getMentor()->getName()); + } + + public function assertForeignKeyIs($value) { + $foreignKey = $this->_em->getConnection()->execute('SELECT mentor_id FROM ecommerce_customers WHERE id=?', array($this->customer->getId()))->fetchColumn(); + $this->assertEquals($value, $foreignKey); + } + + private function _createFixture() { $customer = new ECommerceCustomer; $customer->setName('Luke Skywalker'); @@ -59,22 +95,5 @@ class OneToOneSelfReferentialAssociationTest extends \Doctrine\Tests\OrmFunction $this->_em->flush(); $this->_em->clear(); - - $query = $this->_em->createQuery('select c, m from Doctrine\Tests\Models\ECommerce\ECommerceCustomer c left join c.mentor m order by c.id asc'); - $result = $query->getResultList(); - $customer = $result[0]; - - $this->assertTrue($customer->getMentor() instanceof ECommerceCustomer); - $this->assertEquals('Obi-wan Kenobi', $customer->getMentor()->getName()); - } - - /* TODO: not yet implemented - public function testLazyLoad() { - - }*/ - - public function assertForeignKeyIs($value) { - $foreignKey = $this->_em->getConnection()->execute('SELECT mentor_id FROM ecommerce_customers WHERE id=?', array($this->customer->getId()))->fetchColumn(); - $this->assertEquals($value, $foreignKey); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php b/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php index 38efdef38..9f9e09311 100644 --- a/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php @@ -4,6 +4,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\ECommerce\ECommerceCart; use Doctrine\Tests\Models\ECommerce\ECommerceCustomer; +use Doctrine\Tests\Models\ECommerce\ECommerceProduct; use Doctrine\ORM\Mapping\AssociationMapping; require_once __DIR__ . '/../../TestInit.php'; @@ -20,7 +21,8 @@ class StandardEntityPersisterTest extends \Doctrine\Tests\OrmFunctionalTestCase parent::setUp(); } - public function testAcceptsForeignKeysAsCriteria() { + public function testAcceptsForeignKeysAsCriteria() + { $this->_em->getConfiguration()->setAllowPartialObjects(false); $customer = new ECommerceCustomer(); @@ -38,4 +40,30 @@ class StandardEntityPersisterTest extends \Doctrine\Tests\OrmFunctionalTestCase $persister->load(array('customer_id' => $customer->getId()), $newCart); $this->assertEquals('Credit card', $newCart->getPayment()); } + + public function testAcceptsJoinTableAsCriteria() + { + $this->_em->getConfiguration()->setAllowPartialObjects(false); + + $cart = new ECommerceCart(); + $product = new ECommerceProduct(); + $product->setName('Star Wars: A New Hope'); + $cart->addProduct($product); + $this->_em->persist($cart); + $this->_em->flush(); + $this->_em->clear(); + unset($product); + + $persister = $this->_em->getUnitOfWork()->getEntityPersister('Doctrine\Tests\Models\ECommerce\ECommerceProduct'); + $newProduct = new ECommerceProduct(); + $criteria = array( + array( + 'table' => 'ecommerce_carts_products', + 'join' => array('id' => 'product_id'), + 'criteria' => array('cart_id' => $cart->getId()) + ) + ); + $persister->load($criteria, $newProduct); + $this->assertEquals('Star Wars: A New Hope', $newProduct->getName()); + } } diff --git a/tests/Doctrine/Tests/ORM/PersistentCollectionTest.php b/tests/Doctrine/Tests/ORM/PersistentCollectionTest.php new file mode 100644 index 000000000..c332a24e3 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/PersistentCollectionTest.php @@ -0,0 +1,52 @@ + + */ +class PersistentCollectionTest extends \Doctrine\Tests\OrmTestCase +{ + private $_connectionMock; + private $_emMock; + + protected function setUp() + { + parent::setUp(); + // SUT + $this->_connectionMock = new ConnectionMock(array(), new \Doctrine\Tests\Mocks\DriverMock()); + $this->_emMock = EntityManagerMock::create($this->_connectionMock); + } + + public function testCanBePutInLazyLoadingMode() + { + $class = $this->_emMock->getClassMetadata('Doctrine\Tests\Models\ECommerce\ECommerceProduct'); + $collection = new PersistentCollection($this->_emMock, $class); + $collection->setLazyInitialization(); + } + + public function testQueriesAssociationToLoadItself() + { + $class = $this->_emMock->getClassMetadata('Doctrine\Tests\Models\ECommerce\ECommerceProduct'); + $collection = new PersistentCollection($this->_emMock, $class); + $collection->setLazyInitialization(); + + $association = $this->getMock('Doctrine\ORM\Mapping\OneToManyMapping', array('load'), array(), '', false, false, false); + $association->targetEntityName = 'Doctrine\Tests\Models\ECommerce\ECommerceFeature'; + $product = new ECommerceProduct(); + $association->expects($this->once()) + ->method('load') + ->with($product, $this->isInstanceOf($collection), $this->isInstanceOf($this->_emMock)); + $collection->setOwner($product, $association); + + count($collection); + } +} diff --git a/tests/Doctrine/Tests/ORM/Proxy/ProxyClassGeneratorTest.php b/tests/Doctrine/Tests/ORM/Proxy/ProxyClassGeneratorTest.php index df4740cf0..63079c2ce 100644 --- a/tests/Doctrine/Tests/ORM/Proxy/ProxyClassGeneratorTest.php +++ b/tests/Doctrine/Tests/ORM/Proxy/ProxyClassGeneratorTest.php @@ -90,7 +90,7 @@ class ProxyClassGeneratorTest extends \Doctrine\Tests\OrmTestCase $proxyClass = $this->_generator->generateReferenceProxyClass('Doctrine\Tests\Models\ECommerce\ECommerceFeature'); $persister = $this->_getMockPersister(); $proxy = new $proxyClass($persister, $identifier); - $persister->expects($this->any()) + $persister->expects($this->atLeastOnce()) ->method('load') ->with($this->equalTo($identifier), $this->isInstanceOf($proxyClass)); $proxy->getDescription(); @@ -102,7 +102,7 @@ class ProxyClassGeneratorTest extends \Doctrine\Tests\OrmTestCase $proxyClass = $this->_generator->generateReferenceProxyClass('Doctrine\Tests\Models\ECommerce\ECommerceFeature'); $persister = $this->_getMockPersister(); $proxy = new $proxyClass($persister, $identifier); - $persister->expects($this->once()) + $persister->expects($this->atLeastOnce()) ->method('load') ->with($this->equalTo($identifier), $this->isInstanceOf($proxyClass)); $proxy->getId(); @@ -165,7 +165,7 @@ class ProxyClassGeneratorTest extends \Doctrine\Tests\OrmTestCase $foreignKeys = array('customer_id' => 42); $assoc = $this->_getAssociationMock(); $proxy = new $proxyClass($this->_emMock, $assoc, $product, $foreignKeys); - $assoc->expects($this->any()) + $assoc->expects($this->atLeastOnce()) ->method('load') ->with($product, $this->isInstanceOf($proxyClass), $this->isInstanceOf('Doctrine\Tests\Mocks\EntityManagerMock'), $foreignKeys); $proxy->getDescription(); diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index c5e98b456..3700e1e95 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -66,6 +66,29 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase ); } + public function testSupportsOrderByWithAscAsDefault() + { + $this->assertSqlGeneration( + 'SELECT u FROM Doctrine\Tests\Models\Forum\ForumUser u ORDER BY u.id', + 'SELECT f0_.id AS id0, f0_.username AS username1 FROM forum_users f0_ ORDER BY f0_.id ASC ' + ); + } + + public function testSupportsOrderByAsc() + { + $this->assertSqlGeneration( + 'SELECT u FROM Doctrine\Tests\Models\Forum\ForumUser u ORDER BY u.id asc', + 'SELECT f0_.id AS id0, f0_.username AS username1 FROM forum_users f0_ ORDER BY f0_.id ASC ' + ); + } + public function testSupportsOrderByDesc() + { + $this->assertSqlGeneration( + 'SELECT u FROM Doctrine\Tests\Models\Forum\ForumUser u ORDER BY u.id desc', + 'SELECT f0_.id AS id0, f0_.username AS username1 FROM forum_users f0_ ORDER BY f0_.id DESC ' + ); + } + public function testSupportsSelectDistinct() { $this->assertSqlGeneration( @@ -339,4 +362,4 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase $this->assertEquals('SELECT c0_.id AS id0, c0_.status AS status1, c0_.username AS username2, c0_.name AS name3 FROM cms_users c0_ OFFSET 0 LIMIT 10', $q->getSql()); } -} \ No newline at end of file +}