diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index c0dbfafcc..6216d75dc 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -56,6 +56,17 @@ + + + + + + + + + + + @@ -63,6 +74,7 @@ + @@ -76,6 +88,7 @@ + diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index b1b24230b..787770a34 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -1,7 +1,5 @@ _expireResultCache; } + /** + * Change the default fetch mode of an association for this query. + * + * $fetchMode can be one of ClassMetadata::FETCH_EAGER or ClassMetadata::FETCH_LAZY + * + * @param string $class + * @param string $assocName + * @param int $fetchMode + * @return AbstractQuery + */ + public function setFetchMode($class, $assocName, $fetchMode) + { + if ($fetchMode !== Mapping\ClassMetadata::FETCH_EAGER) { + $fetchMode = Mapping\ClassMetadata::FETCH_LAZY; + } + + $this->_hints['fetchMode'][$class][$assocName] = $fetchMode; + return $this; + } + /** * Defines the processing mode to be used during hydration / result set transformation. * @@ -390,6 +413,31 @@ abstract class AbstractQuery return $this->execute(array(), self::HYDRATE_SCALAR); } + /** + * Get exactly one result or null. + * + * @throws NonUniqueResultException + * @param int $hydrationMode + * @return mixed + */ + public function getOneOrNullResult($hydrationMode = null) + { + $result = $this->execute(array(), $hydrationMode); + + if ($this->_hydrationMode !== self::HYDRATE_SINGLE_SCALAR && ! $result) { + return null; + } + + if (is_array($result)) { + if (count($result) > 1) { + throw new NonUniqueResultException; + } + return array_shift($result); + } + + return $result; + } + /** * Gets the single result of the query. * @@ -476,10 +524,20 @@ abstract class AbstractQuery * @param integer $hydrationMode The hydration mode to use. * @return IterableResult */ - public function iterate(array $params = array(), $hydrationMode = self::HYDRATE_OBJECT) + public function iterate(array $params = array(), $hydrationMode = null) { + if ($hydrationMode !== null) { + $this->setHydrationMode($hydrationMode); + } + + if ($params) { + $this->setParameters($params); + } + + $stmt = $this->_doExecute(); + return $this->_em->newHydrator($this->_hydrationMode)->iterate( - $this->_doExecute($params, $hydrationMode), $this->_resultSetMapping, $this->_hints + $stmt, $this->_resultSetMapping, $this->_hints ); } diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 2839099fc..0379cc435 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -97,12 +97,16 @@ class EntityManager implements ObjectManager private $proxyFactory; /** - * @var ExpressionBuilder The expression builder instance used to generate query expressions. + * The expression builder instance used to generate query expressions. + * + * @var Doctrine\ORM\Query\Expr */ private $expressionBuilder; /** * Whether the EntityManager is closed or not. + * + * @var bool */ private $closed = false; @@ -164,7 +168,7 @@ class EntityManager implements ObjectManager * ->where($expr->orX($expr->eq('u.id', 1), $expr->eq('u.id', 2))); * * - * @return ExpressionBuilder + * @return Doctrine\ORM\Query\Expr */ public function getExpressionBuilder() { @@ -199,13 +203,18 @@ class EntityManager implements ObjectManager public function transactional(Closure $func) { $this->conn->beginTransaction(); + try { - $func($this); + $return = $func($this); + $this->flush(); $this->conn->commit(); + + return $return ?: true; } catch (Exception $e) { $this->close(); $this->conn->rollback(); + throw $e; } } @@ -679,6 +688,9 @@ class EntityManager implements ObjectManager case Query::HYDRATE_SINGLE_SCALAR: $hydrator = new Internal\Hydration\SingleScalarHydrator($this); break; + case Query::HYDRATE_SIMPLEOBJECT: + $hydrator = new Internal\Hydration\SimpleObjectHydrator($this); + break; default: if ($class = $this->config->getCustomHydrationMode($hydrationMode)) { $hydrator = new $class($this); diff --git a/lib/Doctrine/ORM/EntityRepository.php b/lib/Doctrine/ORM/EntityRepository.php index a92ce7355..de2689db2 100644 --- a/lib/Doctrine/ORM/EntityRepository.php +++ b/lib/Doctrine/ORM/EntityRepository.php @@ -78,6 +78,17 @@ class EntityRepository implements ObjectRepository ->from($this->_entityName, $alias); } + /** + * Create a new Query instance based on a predefined metadata named query. + * + * @param string $queryName + * @return Query + */ + public function createNamedQuery($queryName) + { + return $this->_em->createQuery($this->_class->getNamedQuery($queryName)); + } + /** * Clears the repository, causing all managed entities to become detached. */ @@ -149,11 +160,14 @@ class EntityRepository implements ObjectRepository * Finds entities by a set of criteria. * * @param array $criteria - * @return array + * @param array|null $orderBy + * @param int|null $limit + * @param int|null $offset + * @return array The objects. */ - public function findBy(array $criteria) + public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) { - return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->loadAll($criteria); + return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->loadAll($criteria, $orderBy, $limit, $offset); } /** diff --git a/lib/Doctrine/ORM/Event/OnClearEventArgs.php b/lib/Doctrine/ORM/Event/OnClearEventArgs.php new file mode 100644 index 000000000..ad89fbc90 --- /dev/null +++ b/lib/Doctrine/ORM/Event/OnClearEventArgs.php @@ -0,0 +1,54 @@ +. +*/ + +namespace Doctrine\ORM\Event; + +/** + * Provides event arguments for the onClear event. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @version $Revision$ + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class OnClearEventArgs extends \Doctrine\Common\EventArgs +{ + /** + * @var \Doctrine\ORM\EntityManager + */ + private $em; + + /** + * @param \Doctrine\ORM\EntityManager $em + */ + public function __construct($em) + { + $this->em = $em; + } + + /** + * @return \Doctrine\ORM\EntityManager + */ + public function getEntityManager() + { + return $this->em; + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Events.php b/lib/Doctrine/ORM/Events.php index e25de658a..8344b07c1 100644 --- a/lib/Doctrine/ORM/Events.php +++ b/lib/Doctrine/ORM/Events.php @@ -119,4 +119,12 @@ final class Events * @var string */ const onFlush = 'onFlush'; + + /** + * The onClear event occurs when the EntityManager#clear() operation is invoked, + * after all references to entities have been removed from the unit of work. + * + * @var string + */ + const onClear = 'onClear'; } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php index 302cc6b54..a9697c15a 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php @@ -211,7 +211,16 @@ abstract class AbstractHydrator } if (isset($cache[$key]['isMetaColumn'])) { - $rowData[$dqlAlias][$cache[$key]['fieldName']] = $value; + if (!isset($rowData[$dqlAlias][$cache[$key]['fieldName']]) || $value !== null) { + $rowData[$dqlAlias][$cache[$key]['fieldName']] = $value; + } + continue; + } + + // in an inheritance hierachy the same field could be defined several times. + // We overwrite this value so long we dont have a non-null value, that value we keep. + // Per definition it cannot be that a field is defined several times and has several values. + if (isset($rowData[$dqlAlias][$cache[$key]['fieldName']]) && $value === null) { continue; } diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 202fdc7ff..381fd4ab1 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -39,9 +39,9 @@ class ObjectHydrator extends AbstractHydrator * This local cache is maintained between hydration runs and not cleared. */ private $_ce = array(); - + /* The following parts are reinitialized on every hydration run. */ - + private $_identifierMap; private $_resultPointers; private $_idTemplate; @@ -50,7 +50,7 @@ class ObjectHydrator extends AbstractHydrator private $_initializedCollections = array(); private $_existingCollections = array(); //private $_createdEntities; - + /** @override */ protected function _prepare() @@ -59,6 +59,9 @@ class ObjectHydrator extends AbstractHydrator $this->_resultPointers = $this->_idTemplate = array(); $this->_resultCounter = 0; + if (!isset($this->_hints['deferEagerLoad'])) { + $this->_hints['deferEagerLoad'] = true; + } foreach ($this->_rsm->aliasMap as $dqlAlias => $className) { $this->_identifierMap[$dqlAlias] = array(); @@ -68,10 +71,14 @@ class ObjectHydrator extends AbstractHydrator if ( ! isset($this->_ce[$className])) { $this->_ce[$className] = $class; } - + // Remember which associations are "fetch joined", so that we know where to inject // collection stubs or proxies and where not. if (isset($this->_rsm->relationMap[$dqlAlias])) { + if ( ! isset($this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]])) { + throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $this->_rsm->parentAliasMap[$dqlAlias]); + } + $sourceClassName = $this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]]; $sourceClass = $this->_getClassMetadata($sourceClassName); $assoc = $sourceClass->associationMappings[$this->_rsm->relationMap[$dqlAlias]]; @@ -108,11 +115,17 @@ class ObjectHydrator extends AbstractHydrator */ protected function _cleanup() { + $eagerLoad = (isset($this->_hints['deferEagerLoad'])) && $this->_hints['deferEagerLoad'] == true; + parent::_cleanup(); $this->_identifierMap = $this->_initializedCollections = $this->_existingCollections = $this->_resultPointers = array(); + + if ($eagerLoad) { + $this->_em->getUnitOfWork()->triggerEagerLoads(); + } } /** @@ -176,7 +189,7 @@ class ObjectHydrator extends AbstractHydrator return $value; } - + /** * Gets an entity instance. * @@ -186,7 +199,7 @@ class ObjectHydrator extends AbstractHydrator */ private function _getEntity(array $data, $dqlAlias) { - $className = $this->_rsm->aliasMap[$dqlAlias]; + $className = $this->_rsm->aliasMap[$dqlAlias]; if (isset($this->_rsm->discriminatorColumns[$dqlAlias])) { $discrColumn = $this->_rsm->metaMappings[$this->_rsm->discriminatorColumns[$dqlAlias]]; $className = $this->_ce[$className]->discriminatorMap[$data[$discrColumn]]; @@ -194,7 +207,7 @@ class ObjectHydrator extends AbstractHydrator } return $this->_uow->createEntity($className, $data, $this->_hints); } - + private function _getEntityFromIdentityMap($className, array $data) { $class = $this->_ce[$className]; @@ -208,7 +221,7 @@ class ObjectHydrator extends AbstractHydrator return $this->_uow->tryGetByIdHash($data[$class->identifier[0]], $class->rootEntityName); } } - + /** * Gets a ClassMetadata instance from the local cache. * If the instance is not yet in the local cache, it is loaded into the @@ -266,7 +279,7 @@ class ObjectHydrator extends AbstractHydrator // Hydrate the data chunks foreach ($rowData as $dqlAlias => $data) { $entityName = $this->_rsm->aliasMap[$dqlAlias]; - + if (isset($this->_rsm->parentAliasMap[$dqlAlias])) { // It's a joined result @@ -277,7 +290,7 @@ class ObjectHydrator extends AbstractHydrator // Get a reference to the parent object to which the joined element belongs. if ($this->_rsm->isMixed && isset($this->_rootAliases[$parentAlias])) { - $first = reset($this->_resultPointers); + $first = reset($this->_resultPointers); $parentObject = $this->_resultPointers[$parentAlias][key($first)]; } else if (isset($this->_resultPointers[$parentAlias])) { $parentObject = $this->_resultPointers[$parentAlias]; @@ -302,11 +315,11 @@ class ObjectHydrator extends AbstractHydrator } else if ( ! isset($this->_existingCollections[$collKey])) { $reflFieldValue = $this->_initRelatedCollection($parentObject, $parentClass, $relationField); } - + $indexExists = isset($this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]); $index = $indexExists ? $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false; $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false; - + if ( ! $indexExists || ! $indexIsValid) { if (isset($this->_existingCollections[$collKey])) { // Collection exists, only look for the element in the identity map. @@ -395,6 +408,10 @@ class ObjectHydrator extends AbstractHydrator $result[$key] = $element; $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $key; } + + if (isset($this->_hints['collection'])) { + $this->_hints['collection']->hydrateSet($key, $element); + } } else { if ($this->_rsm->isMixed) { $element = array(0 => $element); @@ -402,6 +419,10 @@ class ObjectHydrator extends AbstractHydrator $result[] = $element; $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $this->_resultCounter; ++$this->_resultCounter; + + if (isset($this->_hints['collection'])) { + $this->_hints['collection']->hydrateAdd($element); + } } // Update result pointer diff --git a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php new file mode 100644 index 000000000..3524f89e9 --- /dev/null +++ b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php @@ -0,0 +1,141 @@ +. + */ + + +namespace Doctrine\ORM\Internal\Hydration; + +use \PDO; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\DBAL\Types\Type; + +class SimpleObjectHydrator extends AbstractHydrator +{ + const REFRESH_ENTITY = 'doctrine_refresh_entity'; + + /** + * @var ClassMetadata + */ + private $class; + + private $declaringClasses = array(); + + protected function _hydrateAll() + { + $result = array(); + $cache = array(); + + while ($row = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { + $this->_hydrateRow($row, $cache, $result); + } + + $this->_em->getUnitOfWork()->triggerEagerLoads(); + + return $result; + } + + protected function _prepare() + { + if (count($this->_rsm->aliasMap) == 1) { + $this->class = $this->_em->getClassMetadata(reset($this->_rsm->aliasMap)); + if ($this->class->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { + foreach ($this->_rsm->declaringClasses AS $column => $class) { + $this->declaringClasses[$column] = $this->_em->getClassMetadata($class); + } + } + } else { + throw new \RuntimeException("Cannot use SimpleObjectHydrator with a ResultSetMapping not containing exactly one object result."); + } + if ($this->_rsm->scalarMappings) { + throw new \RuntimeException("Cannot use SimpleObjectHydrator with a ResultSetMapping that contains scalar mappings."); + } + } + + protected function _hydrateRow(array $sqlResult, array &$cache, array &$result) + { + $data = array(); + if ($this->class->inheritanceType == ClassMetadata::INHERITANCE_TYPE_NONE) { + foreach ($sqlResult as $column => $value) { + + if (!isset($cache[$column])) { + if (isset($this->_rsm->fieldMappings[$column])) { + $cache[$column]['name'] = $this->_rsm->fieldMappings[$column]; + $cache[$column]['field'] = true; + } else { + $cache[$column]['name'] = $this->_rsm->metaMappings[$column]; + } + } + + if (isset($cache[$column]['field'])) { + $value = Type::getType($this->class->fieldMappings[$cache[$column]['name']]['type']) + ->convertToPHPValue($value, $this->_platform); + } + $data[$cache[$column]['name']] = $value; + } + $entityName = $this->class->name; + } else { + $discrColumnName = $this->_platform->getSQLResultCasing($this->class->discriminatorColumn['name']); + $entityName = $this->class->discriminatorMap[$sqlResult[$discrColumnName]]; + unset($sqlResult[$discrColumnName]); + foreach ($sqlResult as $column => $value) { + if (!isset($cache[$column])) { + if (isset($this->_rsm->fieldMappings[$column])) { + $field = $this->_rsm->fieldMappings[$column]; + $class = $this->declaringClasses[$column]; + if ($class->name == $entityName || is_subclass_of($entityName, $class->name)) { + $cache[$column]['name'] = $field; + $cache[$column]['class'] = $class; + } + } else if (isset($this->_rsm->relationMap[$column])) { + if ($this->_rsm->relationMap[$column] == $entityName || is_subclass_of($entityName, $this->_rsm->relationMap[$column])) { + $cache[$column]['name'] = $field; + } + } else { + $cache[$column]['name'] = $this->_rsm->metaMappings[$column]; + } + } + + if (isset($cache[$column]['class'])) { + $value = Type::getType($cache[$column]['class']->fieldMappings[$cache[$column]['name']]['type']) + ->convertToPHPValue($value, $this->_platform); + } + + // the second and part is to prevent overwrites in case of multiple + // inheritance classes using the same property name (See AbstractHydrator) + if (isset($cache[$column]) && (!isset($data[$cache[$column]['name']]) || $value !== null)) { + $data[$cache[$column]['name']] = $value; + } + } + } + + if (isset($this->_hints[self::REFRESH_ENTITY])) { + $this->_hints[Query::HINT_REFRESH] = true; + $id = array(); + if ($this->_class->isIdentifierComposite) { + foreach ($this->_class->identifier as $fieldName) { + $id[$fieldName] = $data[$fieldName]; + } + } else { + $id = array($this->_class->identifier[0] => $data[$this->_class->identifier[0]]); + } + $this->_em->getUnitOfWork()->registerManaged($this->_hints[self::REFRESH_ENTITY], $id, $data); + } + + $result[] = $this->_em->getUnitOfWork()->createEntity($entityName, $data, $this->_hints); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadata.php b/lib/Doctrine/ORM/Mapping/ClassMetadata.php index f1b374c5d..4ec0ede53 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadata.php @@ -63,10 +63,10 @@ class ClassMetadata extends ClassMetadataInfo */ public function __construct($entityName) { - parent::__construct($entityName); $this->reflClass = new ReflectionClass($entityName); $this->namespace = $this->reflClass->getNamespaceName(); $this->table['name'] = $this->reflClass->getShortName(); + parent::__construct($this->reflClass->getName()); // do not use $entityName, possible case-problems } /** @@ -330,6 +330,14 @@ class ClassMetadata extends ClassMetadataInfo $serialized[] = 'lifecycleCallbacks'; } + if ($this->namedQueries) { + $serialized[] = 'namedQueries'; + } + + if ($this->isReadOnly) { + $serialized[] = 'isReadOnly'; + } + return $serialized; } diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index b482b1c5e..f556257f9 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -204,6 +204,13 @@ class ClassMetadataInfo implements ClassMetadata */ public $subClasses = array(); + /** + * READ-ONLY: The named queries allowed to be called directly from Repository. + * + * @var array + */ + public $namedQueries = array(); + /** * READ-ONLY: The field names of all fields that are part of the identifier/primary key * of the mapped entity class. @@ -477,6 +484,17 @@ class ClassMetadataInfo implements ClassMetadata */ public $reflClass; + /** + * Is this entity marked as "read-only"? + * + * That means it is never considered for change-tracking in the UnitOfWork. It is a very helpful performance + * optimization for entities that are immutable, either in your domain or through the relation database + * (coming from a view, or a history table for example). + * + * @var bool + */ + public $isReadOnly = false; + /** * Initializes a new ClassMetadata instance that will hold the object-relational mapping * metadata of the class with the given name. @@ -655,6 +673,32 @@ class ClassMetadataInfo implements ClassMetadata $this->fieldNames[$columnName] : $columnName; } + /** + * Gets the named query. + * + * @see ClassMetadataInfo::$namedQueries + * @throws MappingException + * @param string $queryName The query name + * @return string + */ + public function getNamedQuery($queryName) + { + if ( ! isset($this->namedQueries[$queryName])) { + throw MappingException::queryNotFound($this->name, $queryName); + } + return $this->namedQueries[$queryName]; + } + + /** + * Gets all named queries of the class. + * + * @return array + */ + public function getNamedQueries() + { + return $this->namedQueries; + } + /** * Validates & completes the given field mapping. * @@ -1368,8 +1412,7 @@ class ClassMetadataInfo implements ClassMetadata * Adds an association mapping without completing/validating it. * This is mainly used to add inherited association mappings to derived classes. * - * @param AssociationMapping $mapping - * @param string $owningClassName The name of the class that defined this mapping. + * @param array $mapping */ public function addInheritedAssociationMapping(array $mapping/*, $owningClassName = null*/) { @@ -1385,7 +1428,6 @@ class ClassMetadataInfo implements ClassMetadata * This is mainly used to add inherited field mappings to derived classes. * * @param array $mapping - * @todo Rename: addInheritedFieldMapping */ public function addInheritedFieldMapping(array $fieldMapping) { @@ -1394,6 +1436,22 @@ class ClassMetadataInfo implements ClassMetadata $this->fieldNames[$fieldMapping['columnName']] = $fieldMapping['fieldName']; } + /** + * INTERNAL: + * Adds a named query to this class. + * + * @throws MappingException + * @param array $queryMapping + */ + public function addNamedQuery(array $queryMapping) + { + if (isset($this->namedQueries[$queryMapping['name']])) { + throw MappingException::duplicateQueryMapping($this->name, $queryMapping['name']); + } + $query = str_replace('__CLASS__', $this->name, $queryMapping['query']); + $this->namedQueries[$queryMapping['name']] = $query; + } + /** * Adds a one-to-one mapping. * @@ -1570,6 +1628,7 @@ class ClassMetadataInfo implements ClassMetadata if (strpos($className, '\\') === false && strlen($this->namespace)) { $className = $this->namespace . '\\' . $className; } + $className = ltrim($className, '\\'); $this->discriminatorMap[$value] = $className; if ($this->name == $className) { $this->discriminatorValue = $value; @@ -1584,6 +1643,17 @@ class ClassMetadataInfo implements ClassMetadata } } + /** + * Checks whether the class has a named query with the given query name. + * + * @param string $fieldName + * @return boolean + */ + public function hasNamedQuery($queryName) + { + return isset($this->namedQueries[$queryName]); + } + /** * Checks whether the class has a mapped association with the given field name. * @@ -1760,4 +1830,14 @@ class ClassMetadataInfo implements ClassMetadata { $this->versionField = $versionField; } + + /** + * Mark this class as read only, no change tracking is applied to it. + * + * @return void + */ + public function markReadOnly() + { + $this->isReadOnly = true; + } } diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 01a24f7d4..22e6d4d7e 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -67,10 +67,10 @@ class AnnotationDriver implements Driver * Initializes a new AnnotationDriver that uses the given AnnotationReader for reading * docblock annotations. * - * @param $reader The AnnotationReader to use. + * @param AnnotationReader $reader The AnnotationReader to use, duck-typed. * @param string|array $paths One or multiple paths where mapping classes can be found. */ - public function __construct(AnnotationReader $reader, $paths = null) + public function __construct($reader, $paths = null) { $this->_reader = $reader; if ($paths) { @@ -132,6 +132,10 @@ class AnnotationDriver implements Driver if (isset($classAnnotations['Doctrine\ORM\Mapping\Entity'])) { $entityAnnot = $classAnnotations['Doctrine\ORM\Mapping\Entity']; $metadata->setCustomRepositoryClass($entityAnnot->repositoryClass); + + if ($entityAnnot->readOnly) { + $metadata->markReadOnly(); + } } else if (isset($classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass'])) { $metadata->isMappedSuperclass = true; } else { @@ -165,6 +169,18 @@ class AnnotationDriver implements Driver $metadata->setPrimaryTable($primaryTable); } + // Evaluate NamedQueries annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\NamedQueries'])) { + $namedQueriesAnnot = $classAnnotations['Doctrine\ORM\Mapping\NamedQueries']; + + foreach ($namedQueriesAnnot->value as $namedQuery) { + $metadata->addNamedQuery(array( + 'name' => $namedQuery->name, + 'query' => $namedQuery->query + )); + } + } + // Evaluate InheritanceType annotation if (isset($classAnnotations['Doctrine\ORM\Mapping\InheritanceType'])) { $inheritanceTypeAnnot = $classAnnotations['Doctrine\ORM\Mapping\InheritanceType']; diff --git a/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php b/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php index 0dc6a05e0..7f92df162 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php @@ -67,6 +67,26 @@ class DatabaseDriver implements Driver $this->_sm = $schemaManager; } + /** + * Set tables manually instead of relying on the reverse engeneering capabilities of SchemaManager. + * + * @param array $entityTables + * @param array $manyToManyTables + * @return void + */ + public function setTables($entityTables, $manyToManyTables) + { + $this->tables = $this->manyToManyTables = $this->classToTableNames = array(); + foreach ($entityTables AS $table) { + $className = Inflector::classify(strtolower($table->getName())); + $this->classToTableNames[$className] = $table->getName(); + $this->tables[$table->getName()] = $table; + } + foreach ($manyToManyTables AS $table) { + $this->manyToManyTables[$table->getName()] = $table; + } + } + private function reverseEngineerMappingFromDatabase() { if ($this->tables !== null) { @@ -77,7 +97,7 @@ class DatabaseDriver implements Driver $tables[$tableName] = $this->_sm->listTableDetails($tableName); } - $this->tables = array(); + $this->tables = $this->manyToManyTables = $this->classToTableNames = array(); foreach ($tables AS $tableName => $table) { /* @var $table Table */ if ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) { @@ -95,11 +115,7 @@ class DatabaseDriver implements Driver sort($pkColumns); sort($allForeignKeyColumns); - if ($pkColumns == $allForeignKeyColumns) { - if (count($table->getForeignKeys()) > 2) { - throw new \InvalidArgumentException("ManyToMany table '" . $tableName . "' with more or less than two foreign keys are not supported by the Database Reverese Engineering Driver."); - } - + if ($pkColumns == $allForeignKeyColumns && count($foreignKeys) == 2) { $this->manyToManyTables[$tableName] = $table; } else { // lower-casing is necessary because of Oracle Uppercase Tablenames, @@ -191,8 +207,10 @@ class DatabaseDriver implements Driver foreach ($this->manyToManyTables AS $manyTable) { foreach ($manyTable->getForeignKeys() AS $foreignKey) { + // foreign key maps to the table of the current entity, many to many association probably exists if (strtolower($tableName) == strtolower($foreignKey->getForeignTableName())) { $myFk = $foreignKey; + $otherFk = null; foreach ($manyTable->getForeignKeys() AS $foreignKey) { if ($foreignKey != $myFk) { $otherFk = $foreignKey; @@ -200,6 +218,12 @@ class DatabaseDriver implements Driver } } + if (!$otherFk) { + // the definition of this many to many table does not contain + // enough foreign key information to continue reverse engeneering. + continue; + } + $localColumn = current($myFk->getColumns()); $associationMapping = array(); $associationMapping['fieldName'] = Inflector::camelize(str_replace('_id', '', strtolower(current($otherFk->getColumns())))); diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index ef566a083..84d2cd1c8 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -1,7 +1,5 @@ setCustomRepositoryClass( isset($xmlRoot['repository-class']) ? (string)$xmlRoot['repository-class'] : null ); + if (isset($xmlRoot['read-only']) && $xmlRoot['read-only'] == "true") { + $metadata->markReadOnly(); + } } else if ($xmlRoot->getName() == 'mapped-superclass') { $metadata->isMappedSuperclass = true; } else { @@ -69,6 +72,16 @@ class XmlDriver extends AbstractFileDriver $metadata->setPrimaryTable($table); + // Evaluate named queries + if (isset($xmlRoot['named-queries'])) { + foreach ($xmlRoot->{'named-queries'}->{'named-query'} as $namedQueryElement) { + $metadata->addNamedQuery(array( + 'name' => (string)$namedQueryElement['name'], + 'query' => (string)$namedQueryElement['query'] + )); + } + } + /* not implemented specially anyway. use table = schema.table if (isset($xmlRoot['schema'])) { $metadata->table['schema'] = (string)$xmlRoot['schema']; diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index 0a66a156e..97617a64d 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -49,6 +49,9 @@ class YamlDriver extends AbstractFileDriver $metadata->setCustomRepositoryClass( isset($element['repositoryClass']) ? $element['repositoryClass'] : null ); + if (isset($element['readOnly']) && $element['readOnly'] == true) { + $metadata->markReadOnly(); + } } else if ($element['type'] == 'mappedSuperclass') { $metadata->isMappedSuperclass = true; } else { @@ -62,6 +65,21 @@ class YamlDriver extends AbstractFileDriver } $metadata->setPrimaryTable($table); + // Evaluate named queries + if (isset($element['namedQueries'])) { + foreach ($element['namedQueries'] as $name => $queryMapping) { + if (is_string($queryMapping)) { + $queryMapping = array('query' => $queryMapping); + } + + if ( ! isset($queryMapping['name'])) { + $queryMapping['name'] = $name; + } + + $metadata->addNamedQuery($queryMapping); + } + } + /* not implemented specially anyway. use table = schema.table if (isset($element['schema'])) { $metadata->table['schema'] = $element['schema']; diff --git a/lib/Doctrine/ORM/Mapping/MappingException.php b/lib/Doctrine/ORM/Mapping/MappingException.php index f268d0d99..5652cf04b 100644 --- a/lib/Doctrine/ORM/Mapping/MappingException.php +++ b/lib/Doctrine/ORM/Mapping/MappingException.php @@ -73,6 +73,11 @@ class MappingException extends \Doctrine\ORM\ORMException return new self("No mapping found for field '$fieldName' on class '$className'."); } + public static function queryNotFound($className, $queryName) + { + return new self("No query found named '$queryName' on class '$className'."); + } + public static function oneToManyRequiresMappedBy($fieldName) { return new self("OneToMany mapping on field '$fieldName' requires the 'mappedBy' attribute."); @@ -160,6 +165,10 @@ class MappingException extends \Doctrine\ORM\ORMException return new self('Property "'.$fieldName.'" in "'.$entity.'" was already declared, but it must be declared only once'); } + public static function duplicateQueryMapping($entity, $queryName) { + return new self('Query named "'.$queryName.'" in "'.$entity.'" was already declared, but it must be declared only once'); + } + public static function singleIdNotAllowedOnCompositePrimaryKey($entity) { return new self('Single id is not allowed on composite primary key in entity '.$entity); } diff --git a/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php b/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php index bfe1e60d9..425da798a 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php @@ -28,24 +28,11 @@ use Doctrine\ORM\Mapping\ClassMetadata, * types in the hierarchy. * * @author Roman Borschel + * @author Benjamin Eberlei * @since 2.0 */ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister { - /** - * Map from column names to class metadata instances that declare the field the column is mapped to. - * - * @var array - */ - private $declaringClassMap = array(); - - /** - * Map from column names to class names that declare the field the association with join column is mapped to. - * - * @var array - */ - private $declaringJoinColumnMap = array(); - /** * {@inheritdoc} */ @@ -69,49 +56,12 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister /** * {@inheritdoc} */ - protected function _processSQLResult(array $sqlResult) - { - $data = array(); - $discrColumnName = $this->_platform->getSQLResultCasing($this->_class->discriminatorColumn['name']); - $entityName = $this->_class->discriminatorMap[$sqlResult[$discrColumnName]]; - unset($sqlResult[$discrColumnName]); - foreach ($sqlResult as $column => $value) { - $realColumnName = $this->_resultColumnNames[$column]; - if (isset($this->declaringClassMap[$column])) { - $class = $this->declaringClassMap[$column]; - if ($class->name == $entityName || is_subclass_of($entityName, $class->name)) { - $field = $class->fieldNames[$realColumnName]; - if (isset($data[$field])) { - $data[$realColumnName] = $value; - } else { - $data[$field] = Type::getType($class->fieldMappings[$field]['type']) - ->convertToPHPValue($value, $this->_platform); - } - } - } else if (isset($this->declaringJoinColumnMap[$column])) { - if ($this->declaringJoinColumnMap[$column] == $entityName || is_subclass_of($entityName, $this->declaringJoinColumnMap[$column])) { - $data[$realColumnName] = $value; - } - } else { - $data[$realColumnName] = $value; - } - } - - return array($entityName, $data); - } - - /** - * {@inheritdoc} - */ - protected function _getSelectColumnSQL($field, ClassMetadata $class) + protected function _getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r') { $columnName = $class->columnNames[$field]; - $sql = $this->_getSQLTableAlias($class->name) . '.' . $class->getQuotedColumnName($field, $this->_platform); + $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) . '.' . $class->getQuotedColumnName($field, $this->_platform); $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++); - if ( ! isset($this->_resultColumnNames[$columnAlias])) { - $this->_resultColumnNames[$columnAlias] = $columnName; - $this->declaringClassMap[$columnAlias] = $class; - } + $this->_rsm->addFieldResult($alias, $columnAlias, $field, $class->name); return "$sql AS $columnAlias"; } @@ -120,10 +70,7 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister { $columnAlias = $joinColumnName . $this->_sqlAliasCounter++; $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); - if ( ! isset($this->_resultColumnNames[$resultColumnName])) { - $this->_resultColumnNames[$resultColumnName] = $joinColumnName; - $this->declaringJoinColumnMap[$resultColumnName] = $className; - } + $this->_rsm->addMetaResult('r', $resultColumnName, $joinColumnName); return $tableAlias . ".$joinColumnName AS $columnAlias"; } diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index c9ae5ddd9..454078dfe 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -22,6 +22,7 @@ namespace Doctrine\ORM\Persisters; use PDO, Doctrine\DBAL\LockMode, Doctrine\DBAL\Types\Type, + Doctrine\DBAL\Connection, Doctrine\ORM\ORMException, Doctrine\ORM\OptimisticLockException, Doctrine\ORM\EntityManager, @@ -69,6 +70,7 @@ use PDO, * * @author Roman Borschel * @author Giorgio Sironi + * @author Benjamin Eberlei * @since 2.0 */ class BasicEntityPersister @@ -107,15 +109,15 @@ class BasicEntityPersister * @var array */ protected $_queuedInserts = array(); - + /** - * Case-sensitive mappings of column names as they appear in an SQL result set - * to column names as they are defined in the mapping. This is necessary because different - * RDBMS vendors return column names in result sets in different casings. + * ResultSetMapping that is used for all queries. Is generated lazily once per request. * - * @var array + * TODO: Evaluate Caching in combination with the other cached SQL snippets. + * + * @var Query\ResultSetMapping */ - protected $_resultColumnNames = array(); + protected $_rsm; /** * The map of column names to DBAL mapping types of all prepared columns used @@ -142,6 +144,14 @@ class BasicEntityPersister * @var string */ protected $_selectColumnListSql; + + /** + * The JOIN SQL fragement used to eagerly load all many-to-one and one-to-one + * associations configured as FETCH_EAGER, aswell as all inverse one-to-one associations. + * + * @var string + */ + protected $_selectJoinSql; /** * Counter for creating unique SQL table and column aliases. @@ -365,7 +375,7 @@ class BasicEntityPersister $result = $this->_conn->executeUpdate($sql, $params, $types); - if ($this->_class->isVersioned && ! $result) { + if ($versioned && ! $result) { throw OptimisticLockException::lockFailed($entity); } } @@ -557,10 +567,18 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, $lockMode); list($params, $types) = $this->expandParameters($criteria); $stmt = $this->_conn->executeQuery($sql, $params, $types); - $result = $stmt->fetch(PDO::FETCH_ASSOC); - $stmt->closeCursor(); + + if ($entity !== null) { + $hints[Query::HINT_REFRESH] = true; + } - return $this->_createEntity($result, $entity, $hints); + if ($this->_selectJoinSql) { + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + } else { + $hydrator = $this->_em->newHydrator(Query::HYDRATE_SIMPLEOBJECT); + } + $entities = $hydrator->hydrateAll($stmt, $this->_rsm, $hints); + return $entities ? $entities[0] : null; } /** @@ -577,6 +595,10 @@ class BasicEntityPersister */ public function loadOneToOneEntity(array $assoc, $sourceEntity, $targetEntity, array $identifier = array()) { + if ($foundEntity = $this->_em->getUnitOfWork()->tryGetById($identifier, $assoc['targetEntity'])) { + return $foundEntity; + } + $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); if ($assoc['isOwningSide']) { @@ -641,79 +663,9 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($id); list($params, $types) = $this->expandParameters($id); $stmt = $this->_conn->executeQuery($sql, $params, $types); - $result = $stmt->fetch(PDO::FETCH_ASSOC); - $stmt->closeCursor(); - - $metaColumns = array(); - $newData = array(); - - // Refresh simple state - foreach ($result as $column => $value) { - $column = $this->_resultColumnNames[$column]; - if (isset($this->_class->fieldNames[$column])) { - $fieldName = $this->_class->fieldNames[$column]; - $newValue = $this->_conn->convertToPHPValue($value, $this->_class->fieldMappings[$fieldName]['type']); - $this->_class->reflFields[$fieldName]->setValue($entity, $newValue); - $newData[$fieldName] = $newValue; - } else { - $metaColumns[$column] = $value; - } - } - - // Refresh associations - foreach ($this->_class->associationMappings as $field => $assoc) { - $value = $this->_class->reflFields[$field]->getValue($entity); - if ($assoc['type'] & ClassMetadata::TO_ONE) { - if ($value instanceof Proxy && ! $value->__isInitialized__) { - continue; // skip uninitialized proxies - } - - if ($assoc['isOwningSide']) { - $joinColumnValues = array(); - foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { - if ($metaColumns[$srcColumn] !== null) { - $joinColumnValues[$targetColumn] = $metaColumns[$srcColumn]; - } - } - if ( ! $joinColumnValues && $value !== null) { - $this->_class->reflFields[$field]->setValue($entity, null); - $newData[$field] = null; - } else if ($value !== null) { - // Check identity map first, if the entity is not there, - // place a proxy in there instead. - $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); - if ($found = $this->_em->getUnitOfWork()->tryGetById($joinColumnValues, $targetClass->rootEntityName)) { - $this->_class->reflFields[$field]->setValue($entity, $found); - // Complete inverse side, if necessary. - if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) { - $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']]; - $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($found, $entity); - } - $newData[$field] = $found; - } else { - // FIXME: What is happening with subClassees here? - $proxy = $this->_em->getProxyFactory()->getProxy($assoc['targetEntity'], $joinColumnValues); - $this->_class->reflFields[$field]->setValue($entity, $proxy); - $newData[$field] = $proxy; - $this->_em->getUnitOfWork()->registerManaged($proxy, $joinColumnValues, array()); - } - } - } else { - // Inverse side of 1-1/1-x can never be lazy. - //$newData[$field] = $assoc->load($entity, null, $this->_em); - $newData[$field] = $this->_em->getUnitOfWork()->getEntityPersister($assoc['targetEntity']) - ->loadOneToOneEntity($assoc, $entity, null); - } - } else if ($value instanceof PersistentCollection && $value->isInitialized()) { - $value->setInitialized(false); - // no matter if dirty or non-dirty entities are already loaded, smoke them out! - // the beauty of it being, they are still in the identity map - $value->unwrap()->clear(); - $newData[$field] = $value; - } - } - - $this->_em->getUnitOfWork()->setOriginalEntityData($entity, $newData); + + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + $hydrator->hydrateAll($stmt, $this->_rsm, array(Query::HINT_REFRESH => true)); if (isset($this->_class->lifecycleCallbacks[Events::postLoad])) { $this->_class->invokeLifecycleCallbacks(Events::postLoad, $entity); @@ -728,22 +680,24 @@ class BasicEntityPersister * Loads a list of entities by a list of field criteria. * * @param array $criteria + * @param array $orderBy + * @param int $limit + * @param int $offset * @return array */ - public function loadAll(array $criteria = array()) + public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null) { $entities = array(); - $sql = $this->_getSelectEntitiesSQL($criteria); + $sql = $this->_getSelectEntitiesSQL($criteria, null, 0, $limit, $offset, $orderBy); list($params, $types) = $this->expandParameters($criteria); $stmt = $this->_conn->executeQuery($sql, $params, $types); - $result = $stmt->fetchAll(PDO::FETCH_ASSOC); - $stmt->closeCursor(); - foreach ($result as $row) { - $entities[] = $this->_createEntity($row); + if ($this->_selectJoinSql) { + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + } else { + $hydrator = $this->_em->newHydrator(Query::HYDRATE_SIMPLEOBJECT); } - - return $entities; + return $hydrator->hydrateAll($stmt, $this->_rsm, array('deferEagerLoads' => true)); } /** @@ -770,19 +724,17 @@ class BasicEntityPersister */ private function loadArrayFromStatement($assoc, $stmt) { - $entities = array(); + $hints = array('deferEagerLoads' => true); + if (isset($assoc['indexBy'])) { - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $entity = $this->_createEntity($result); - $entities[$this->_class->reflFields[$assoc['indexBy']]->getValue($entity)] = $entity; - } + $rsm = clone ($this->_rsm); // this is necessary because the "default rsm" should be changed. + $rsm->addIndexBy('r', $assoc['indexBy']); } else { - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $entities[] = $this->_createEntity($result); - } + $rsm = $this->_rsm; } - $stmt->closeCursor(); - return $entities; + + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + return $hydrator->hydrateAll($stmt, $rsm, $hints); } /** @@ -793,18 +745,18 @@ class BasicEntityPersister * @param PersistentCollection $coll */ private function loadCollectionFromStatement($assoc, $stmt, $coll) - { + { + $hints = array('deferEagerLoads' => true, 'collection' => $coll); + if (isset($assoc['indexBy'])) { - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $entity = $this->_createEntity($result); - $coll->hydrateSet($this->_class->reflFields[$assoc['indexBy']]->getValue($entity), $entity); - } + $rsm = clone ($this->_rsm); // this is necessary because the "default rsm" should be changed. + $rsm->addIndexBy('r', $assoc['indexBy']); } else { - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $coll->hydrateAdd($this->_createEntity($result)); - } + $rsm = $this->_rsm; } - $stmt->closeCursor(); + + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + $hydrator->hydrateAll($stmt, $rsm, $hints); } /** @@ -827,8 +779,8 @@ class BasicEntityPersister { $criteria = array(); $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']); - $joinTableConditions = array(); if ($assoc['isOwningSide']) { + $quotedJoinTable = $sourceClass->getQuotedJoinTableName($assoc, $this->_platform); foreach ($assoc['relationToSourceKeyColumns'] as $relationKeyColumn => $sourceKeyColumn) { if ($sourceClass->containsForeignIdentifier) { $field = $sourceClass->getFieldForColumn($sourceKeyColumn); @@ -837,9 +789,10 @@ class BasicEntityPersister $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value); $value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]]; } - $criteria[$relationKeyColumn] = $value; + + $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $value; } else if (isset($sourceClass->fieldNames[$sourceKeyColumn])) { - $criteria[$relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); } else { throw MappingException::joinColumnMustPointToMappedField( $sourceClass->name, $sourceKeyColumn @@ -848,6 +801,7 @@ class BasicEntityPersister } } else { $owningAssoc = $this->_em->getClassMetadata($assoc['targetEntity'])->associationMappings[$assoc['mappedBy']]; + $quotedJoinTable = $sourceClass->getQuotedJoinTableName($owningAssoc, $this->_platform); // TRICKY: since the association is inverted source and target are flipped foreach ($owningAssoc['relationToTargetKeyColumns'] as $relationKeyColumn => $sourceKeyColumn) { if ($sourceClass->containsForeignIdentifier) { @@ -857,9 +811,9 @@ class BasicEntityPersister $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value); $value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]]; } - $criteria[$relationKeyColumn] = $value; + $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $value; } else if (isset($sourceClass->fieldNames[$sourceKeyColumn])) { - $criteria[$relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); } else { throw MappingException::joinColumnMustPointToMappedField( $sourceClass->name, $sourceKeyColumn @@ -873,72 +827,6 @@ class BasicEntityPersister return $this->_conn->executeQuery($sql, $params, $types); } - /** - * Creates or fills a single entity object from an SQL result. - * - * @param $result The SQL result. - * @param object $entity The entity object to fill, if any. - * @param array $hints Hints for entity creation. - * @return object The filled and managed entity object or NULL, if the SQL result is empty. - */ - private function _createEntity($result, $entity = null, array $hints = array()) - { - if ($result === false) { - return null; - } - - list($entityName, $data) = $this->_processSQLResult($result); - - if ($entity !== null) { - $hints[Query::HINT_REFRESH] = true; - $id = array(); - if ($this->_class->isIdentifierComposite) { - foreach ($this->_class->identifier as $fieldName) { - $id[$fieldName] = $data[$fieldName]; - } - } else { - $id = array($this->_class->identifier[0] => $data[$this->_class->identifier[0]]); - } - $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data); - } - - return $this->_em->getUnitOfWork()->createEntity($entityName, $data, $hints); - } - - /** - * Processes an SQL result set row that contains data for an entity of the type - * this persister is responsible for. - * - * Subclasses are supposed to override this method if they need to change the - * hydration procedure for entities loaded through basic find operations or - * lazy-loading (not DQL). - * - * @param array $sqlResult The SQL result set row to process. - * @return array A tuple where the first value is the actual type of the entity and - * the second value the prepared data of the entity (a map from field - * names to values). - */ - protected function _processSQLResult(array $sqlResult) - { - $data = array(); - foreach ($sqlResult as $column => $value) { - $column = $this->_resultColumnNames[$column]; - if (isset($this->_class->fieldNames[$column])) { - $field = $this->_class->fieldNames[$column]; - if (isset($data[$field])) { - $data[$column] = $value; - } else { - $data[$field] = Type::getType($this->_class->fieldMappings[$field]['type']) - ->convertToPHPValue($value, $this->_platform); - } - } else { - $data[$column] = $value; - } - } - - return array($this->_class->name, $data); - } - /** * Gets the SELECT SQL to select one or more entities by a set of field criteria. * @@ -946,19 +834,21 @@ class BasicEntityPersister * @param AssociationMapping $assoc * @param string $orderBy * @param int $lockMode + * @param int $limit + * @param int $offset + * @param array $orderBy * @return string * @todo Refactor: _getSelectSQL(...) */ - protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null) + protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) { $joinSql = $assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY ? $this->_getSelectManyToManyJoinSQL($assoc) : ''; $conditionSql = $this->_getSelectConditionSQL($criteria, $assoc); - $orderBySql = $assoc !== null && isset($assoc['orderBy']) ? - $this->_getCollectionOrderBySQL($assoc['orderBy'], $this->_getSQLTableAlias($this->_class->name)) - : ''; + $orderBy = ($assoc !== null && isset($assoc['orderBy'])) ? $assoc['orderBy'] : $orderBy; + $orderBySql = $orderBy ? $this->_getOrderBySQL($orderBy, $this->_getSQLTableAlias($this->_class->name)) : ''; $lockSql = ''; if ($lockMode == LockMode::PESSIMISTIC_READ) { @@ -970,7 +860,7 @@ class BasicEntityPersister return $this->_platform->modifyLimitQuery('SELECT ' . $this->_getSelectColumnListSQL() . $this->_platform->appendLockHint(' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($this->_class->name), $lockMode) - . $joinSql + . $this->_selectJoinSql . $joinSql . ($conditionSql ? ' WHERE ' . $conditionSql : '') . $orderBySql, $limit, $offset) . $lockSql; @@ -984,12 +874,12 @@ class BasicEntityPersister * @return string * @todo Rename: _getOrderBySQL */ - protected final function _getCollectionOrderBySQL(array $orderBy, $baseTableAlias) + protected final function _getOrderBySQL(array $orderBy, $baseTableAlias) { $orderBySql = ''; foreach ($orderBy as $fieldName => $orientation) { if ( ! isset($this->_class->fieldMappings[$fieldName])) { - ORMException::unrecognizedField($fieldName); + throw ORMException::unrecognizedField($fieldName); } $tableAlias = isset($this->_class->fieldMappings[$fieldName]['inherited']) ? @@ -1023,6 +913,8 @@ class BasicEntityPersister } $columnList = ''; + $this->_rsm = new Query\ResultSetMapping(); + $this->_rsm->addEntityResult($this->_class->name, 'r'); // r for root // Add regular columns to select list foreach ($this->_class->fieldNames as $field) { @@ -1030,16 +922,53 @@ class BasicEntityPersister $columnList .= $this->_getSelectColumnSQL($field, $this->_class); } - foreach ($this->_class->associationMappings as $assoc) { - if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { - foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { + $this->_selectJoinSql = ''; + $eagerAliasCounter = 0; + foreach ($this->_class->associationMappings as $assocField => $assoc) { + $assocColumnSQL = $this->_getSelectColumnAssociationSQL($assocField, $assoc, $this->_class); + if ($assocColumnSQL) { + if ($columnList) $columnList .= ', '; + $columnList .= $assocColumnSQL; + } + + if ($assoc['type'] & ClassMetadata::TO_ONE && ($assoc['fetch'] == ClassMetadata::FETCH_EAGER || !$assoc['isOwningSide'])) { + $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']); + if ($eagerEntity->inheritanceType != ClassMetadata::INHERITANCE_TYPE_NONE) { + continue; // now this is why you shouldn't use inheritance + } + + $assocAlias = 'e' . ($eagerAliasCounter++); + $this->_rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField); + + foreach ($eagerEntity->fieldNames AS $field) { if ($columnList) $columnList .= ', '; + $columnList .= $this->_getSelectColumnSQL($field, $eagerEntity, $assocAlias); + } + + foreach ($eagerEntity->associationMappings as $assoc2Field => $assoc2) { + $assoc2ColumnSQL = $this->_getSelectColumnAssociationSQL($assoc2Field, $assoc2, $eagerEntity, $assocAlias); + if ($assoc2ColumnSQL) { + if ($columnList) $columnList .= ', '; + $columnList .= $assoc2ColumnSQL; + } + } + $this->_selectJoinSql .= ' LEFT JOIN'; // TODO: Inner join when all join columns are NOT nullable. + if ($assoc['isOwningSide']) { + $this->_selectJoinSql .= ' ' . $eagerEntity->table['name'] . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON '; + + foreach ($assoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) { + $this->_selectJoinSql .= $this->_getSQLTableAlias($assoc['sourceEntity']) . '.'.$sourceCol.' = ' . + $this->_getSQLTableAlias($assoc['targetEntity'], $assocAlias) . '.'.$targetCol.' '; + } + } else { + $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']); + $owningAssoc = $eagerEntity->getAssociationMapping($assoc['mappedBy']); + + $this->_selectJoinSql .= ' ' . $eagerEntity->table['name'] . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON '; - $columnAlias = $srcColumn . $this->_sqlAliasCounter++; - $columnList .= $this->_getSQLTableAlias($this->_class->name) . ".$srcColumn AS $columnAlias"; - $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); - if ( ! isset($this->_resultColumnNames[$resultColumnName])) { - $this->_resultColumnNames[$resultColumnName] = $srcColumn; + foreach ($owningAssoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) { + $this->_selectJoinSql .= $this->_getSQLTableAlias($owningAssoc['sourceEntity'], $assocAlias) . '.'.$sourceCol.' = ' . + $this->_getSQLTableAlias($owningAssoc['targetEntity']) . '.' . $targetCol . ' '; } } } @@ -1049,6 +978,22 @@ class BasicEntityPersister return $this->_selectColumnListSql; } + + protected function _getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class, $alias = 'r') + { + $columnList = ''; + if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { + foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { + if ($columnList) $columnList .= ', '; + + $columnAlias = $srcColumn . $this->_sqlAliasCounter++; + $columnList .= $this->_getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias) ) . ".$srcColumn AS $columnAlias"; + $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); + $this->_rsm->addMetaResult($alias, $this->_platform->getSQLResultCasing($columnAlias), $srcColumn); + } + } + return $columnList; + } /** * Gets the SQL join fragment used when selecting entities from a @@ -1152,15 +1097,14 @@ class BasicEntityPersister * @param string $field The field name. * @param ClassMetadata $class The class that declares this field. The table this class is * mapped to must own the column for the given field. + * @param string $alias */ - protected function _getSelectColumnSQL($field, ClassMetadata $class) + protected function _getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r') { $columnName = $class->columnNames[$field]; - $sql = $this->_getSQLTableAlias($class->name) . '.' . $class->getQuotedColumnName($field, $this->_platform); + $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) . '.' . $class->getQuotedColumnName($field, $this->_platform); $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++); - if ( ! isset($this->_resultColumnNames[$columnAlias])) { - $this->_resultColumnNames[$columnAlias] = $columnName; - } + $this->_rsm->addFieldResult($alias, $columnAlias, $field); return "$sql AS $columnAlias"; } @@ -1172,14 +1116,18 @@ class BasicEntityPersister * @return string The SQL table alias. * @todo Reconsider. Binding table aliases to class names is not such a good idea. */ - protected function _getSQLTableAlias($className) + protected function _getSQLTableAlias($className, $assocName = '') { + if ($assocName) { + $className .= '#'.$assocName; + } + if (isset($this->_sqlTableAliases[$className])) { return $this->_sqlTableAliases[$className]; } $tableAlias = 't' . $this->_sqlAliasCounter++; - $this->_sqlTableAliases[$className] = $tableAlias; + $this->_sqlTableAliases[$className] = $tableAlias; return $tableAlias; } @@ -1254,18 +1202,16 @@ class BasicEntityPersister } $conditionSql .= $this->_class->associationMappings[$field]['joinColumns'][0]['name']; - } else if ($assoc !== null) { - if ($assoc['type'] == ClassMetadata::MANY_TO_MANY) { - $owningAssoc = $assoc['isOwningSide'] ? $assoc : $this->_em->getClassMetadata($assoc['targetEntity']) - ->associationMappings[$assoc['mappedBy']]; - $conditionSql .= $this->_class->getQuotedJoinTableName($owningAssoc, $this->_platform) . '.' . $field; - } else { - $conditionSql .= $field; - } + } else if ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false) { + // very careless developers could potentially open up this normally hidden api for userland attacks, + // therefore checking for spaces and function calls which are not allowed. + + // found a join column condition, not really a "field" + $conditionSql .= $field; } else { throw ORMException::unrecognizedField($field); } - $conditionSql .= ' = ?'; + $conditionSql .= (is_array($value)) ? ' IN (?)' : (($value === null) ? ' IS NULL' : ' = ?'); } return $conditionSql; } @@ -1314,6 +1260,11 @@ class BasicEntityPersister $criteria = array(); $owningAssoc = $this->_class->associationMappings[$assoc['mappedBy']]; $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']); + + $tableAlias = isset($owningAssoc['inherited']) ? + $this->_getSQLTableAlias($owningAssoc['inherited']) + : $this->_getSQLTableAlias($this->_class->name); + foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) { if ($sourceClass->containsForeignIdentifier) { $field = $sourceClass->getFieldForColumn($sourceKeyColumn); @@ -1322,14 +1273,15 @@ class BasicEntityPersister $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value); $value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]]; } - $criteria[$targetKeyColumn] = $value; + $criteria[$tableAlias . "." . $targetKeyColumn] = $value; } else { - $criteria[$targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + $criteria[$tableAlias . "." . $targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); } } $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset); list($params, $types) = $this->expandParameters($criteria); + return $this->_conn->executeQuery($sql, $params, $types); } @@ -1344,10 +1296,18 @@ class BasicEntityPersister $params = $types = array(); foreach ($criteria AS $field => $value) { + if ($value === null) { + continue; // skip null values. + } + $type = null; if (isset($this->_class->fieldMappings[$field])) { $type = Type::getType($this->_class->fieldMappings[$field]['type'])->getBindingType(); } + if (is_array($value)) { + $type += Connection::ARRAY_PARAM_OFFSET; + } + $params[] = $value; $types[] = $type; } diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index 8aa784001..1cb73a9d6 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -21,13 +21,15 @@ namespace Doctrine\ORM\Persisters; use Doctrine\ORM\ORMException, Doctrine\ORM\Mapping\ClassMetadata, - Doctrine\DBAL\LockMode; + Doctrine\DBAL\LockMode, + Doctrine\ORM\Query\ResultSetMapping; /** * The joined subclass persister maps a single entity instance to several tables in the * database as it is defined by the Class Table Inheritance strategy. * * @author Roman Borschel + * @author Benjamin Eberlei * @since 2.0 * @see http://martinfowler.com/eaaCatalog/classTableInheritance.html */ @@ -236,13 +238,17 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister /** * {@inheritdoc} */ - protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null) + protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) { $idColumns = $this->_class->getIdentifierColumnNames(); $baseTableAlias = $this->_getSQLTableAlias($this->_class->name); // Create the column list fragment only once if ($this->_selectColumnListSql === null) { + + $this->_rsm = new ResultSetMapping(); + $this->_rsm->addEntityResult($this->_class->name, 'r'); + // Add regular columns $columnList = ''; foreach ($this->_class->fieldMappings as $fieldName => $mapping) { @@ -278,7 +284,8 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister } $resultColumnName = $this->_platform->getSQLResultCasing($discrColumn); - $this->_resultColumnNames[$resultColumnName] = $discrColumn; + $this->_rsm->setDiscriminatorColumn('r', $discrColumn); + $this->_rsm->addMetaResult('r', $resultColumnName, $discrColumn); } // INNER JOIN parent tables @@ -336,10 +343,8 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister $conditionSql = $this->_getSelectConditionSQL($criteria, $assoc); - $orderBySql = ''; - if ($assoc != null && isset($assoc['orderBy'])) { - $orderBySql = $this->_getCollectionOrderBySQL($assoc['orderBy'], $baseTableAlias); - } + $orderBy = ($assoc !== null && isset($assoc['orderBy'])) ? $assoc['orderBy'] : $orderBy; + $orderBySql = $orderBy ? $this->_getOrderBySQL($orderBy, $baseTableAlias) : ''; if ($this->_selectColumnListSql === null) { $this->_selectColumnListSql = $columnList; diff --git a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php index 0ffa93826..bf9104580 100644 --- a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php +++ b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php @@ -26,6 +26,7 @@ use Doctrine\ORM\Mapping\ClassMetadata; * SINGLE_TABLE strategy. * * @author Roman Borschel + * @author Benjamin Eberlei * @since 2.0 * @link http://martinfowler.com/eaaCatalog/singleTableInheritance.html */ @@ -48,7 +49,8 @@ class SingleTablePersister extends AbstractEntityInheritancePersister $rootClass = $this->_em->getClassMetadata($this->_class->rootEntityName); $tableAlias = $this->_getSQLTableAlias($rootClass->name); $resultColumnName = $this->_platform->getSQLResultCasing($discrColumn); - $this->_resultColumnNames[$resultColumnName] = $discrColumn; + $this->_rsm->setDiscriminatorColumn('r', $discrColumn); + $this->_rsm->addMetaResult('r', $resultColumnName, $discrColumn); // Append subclass columns foreach ($this->_class->subClasses as $subClassName) { @@ -86,9 +88,9 @@ class SingleTablePersister extends AbstractEntityInheritancePersister } /** {@inheritdoc} */ - protected function _getSQLTableAlias($className) + protected function _getSQLTableAlias($className, $assocName = '') { - return parent::_getSQLTableAlias($this->_class->rootEntityName); + return parent::_getSQLTableAlias($this->_class->rootEntityName, $assocName); } /** {@inheritdoc} */ diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index d428e2ddb..fa7fff2e4 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -249,7 +249,12 @@ final class Query extends AbstractQuery $idValues = $class->getIdentifierValues($value); } $sqlPositions = $paramMappings[$key]; - $sqlParams += array_combine((array)$sqlPositions, $idValues); + $cSqlPos = count($sqlPositions); + $cIdValues = count($idValues); + $idValues = array_values($idValues); + for ($i = 0; $i < $cSqlPos; $i++) { + $sqlParams[$sqlPositions[$i]] = $idValues[ ($i % $cIdValues) ]; + } } else { foreach ($paramMappings[$key] as $position) { $sqlParams[$position] = $value; diff --git a/lib/Doctrine/ORM/Query/AST/Functions/DateAddFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/DateAddFunction.php new file mode 100644 index 000000000..1d840cc49 --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/Functions/DateAddFunction.php @@ -0,0 +1,71 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; +use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\Parser; +use Doctrine\ORM\Query\QueryException; + +/** + * "DATE_ADD(date1, interval, unit)" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + */ +class DateAddFunction extends FunctionNode +{ + public $firstDateExpression = null; + public $intervalExpression = null; + public $unit = null; + + public function getSql(SqlWalker $sqlWalker) + { + $unit = strtolower($this->unit); + if ($unit == "day") { + return $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddDaysExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->intervalExpression->dispatch($sqlWalker) + ); + } else if ($unit == "month") { + return $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddMonthExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->intervalExpression->dispatch($sqlWalker) + ); + } else { + throw QueryException::semanticalError('DATE_ADD() only supports units of type day and month.'); + } + } + + public function parse(Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->firstDateExpression = $parser->ArithmeticPrimary(); + $parser->match(Lexer::T_COMMA); + $this->intervalExpression = $parser->ArithmeticPrimary(); + $parser->match(Lexer::T_COMMA); + $this->unit = $parser->StringPrimary(); + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/AST/Functions/DateDiffFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/DateDiffFunction.php new file mode 100644 index 000000000..4de5411bd --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/Functions/DateDiffFunction.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; +use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\Parser; + +/** + * "DATE_DIFF(date1, date2)" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + */ +class DateDiffFunction extends FunctionNode +{ + public $date1; + public $date2; + + public function getSql(SqlWalker $sqlWalker) + { + return $sqlWalker->getConnection()->getDatabasePlatform()->getDateDiffExpression( + $this->date1->dispatch($sqlWalker), + $this->date2->dispatch($sqlWalker) + ); + } + + public function parse(Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->date1 = $parser->ArithmeticPrimary(); + $parser->match(Lexer::T_COMMA); + $this->date2 = $parser->ArithmeticPrimary(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/AST/Functions/DateSubFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/DateSubFunction.php new file mode 100644 index 000000000..a608d49d4 --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/Functions/DateSubFunction.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; +use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\Parser; +use Doctrine\ORM\Query\QueryException; + +/** + * "DATE_ADD(date1, interval, unit)" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + */ +class DateSubFunction extends DateAddFunction +{ + public $firstDateExpression = null; + public $intervalExpression = null; + public $unit = null; + + public function getSql(SqlWalker $sqlWalker) + { + $unit = strtolower($this->unit); + if ($unit == "day") { + return $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubDaysExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->intervalExpression->dispatch($sqlWalker) + ); + } else if ($unit == "month") { + return $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubMonthExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->intervalExpression->dispatch($sqlWalker) + ); + } else { + throw QueryException::semanticalError('DATE_SUB() only supports units of type day and month.'); + } + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/Lexer.php b/lib/Doctrine/ORM/Query/Lexer.php index 673ab0205..82355faec 100644 --- a/lib/Doctrine/ORM/Query/Lexer.php +++ b/lib/Doctrine/ORM/Query/Lexer.php @@ -126,7 +126,7 @@ class Lexer extends \Doctrine\Common\Lexer '[a-z_\\\][a-z0-9_\:\\\]*[a-z0-9_]{1}', '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', "'(?:[^']|'')*'", - '\?[1-9][0-9]*|:[a-z][a-z0-9_]+' + '\?[1-9][0-9]*|:[a-z]{1}[a-z0-9_]{0,}' ); } diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 4caee210c..791f765e9 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -45,19 +45,22 @@ class Parser /** READ-ONLY: Maps BUILT-IN numeric function names to AST class names. */ private static $_NUMERIC_FUNCTIONS = array( - 'length' => 'Doctrine\ORM\Query\AST\Functions\LengthFunction', - 'locate' => 'Doctrine\ORM\Query\AST\Functions\LocateFunction', - 'abs' => 'Doctrine\ORM\Query\AST\Functions\AbsFunction', - 'sqrt' => 'Doctrine\ORM\Query\AST\Functions\SqrtFunction', - 'mod' => 'Doctrine\ORM\Query\AST\Functions\ModFunction', - 'size' => 'Doctrine\ORM\Query\AST\Functions\SizeFunction' + 'length' => 'Doctrine\ORM\Query\AST\Functions\LengthFunction', + 'locate' => 'Doctrine\ORM\Query\AST\Functions\LocateFunction', + 'abs' => 'Doctrine\ORM\Query\AST\Functions\AbsFunction', + 'sqrt' => 'Doctrine\ORM\Query\AST\Functions\SqrtFunction', + 'mod' => 'Doctrine\ORM\Query\AST\Functions\ModFunction', + 'size' => 'Doctrine\ORM\Query\AST\Functions\SizeFunction', + 'date_diff' => 'Doctrine\ORM\Query\AST\Functions\DateDiffFunction', ); /** READ-ONLY: Maps BUILT-IN datetime function names to AST class names. */ private static $_DATETIME_FUNCTIONS = array( 'current_date' => 'Doctrine\ORM\Query\AST\Functions\CurrentDateFunction', 'current_time' => 'Doctrine\ORM\Query\AST\Functions\CurrentTimeFunction', - 'current_timestamp' => 'Doctrine\ORM\Query\AST\Functions\CurrentTimestampFunction' + 'current_timestamp' => 'Doctrine\ORM\Query\AST\Functions\CurrentTimestampFunction', + 'date_add' => 'Doctrine\ORM\Query\AST\Functions\DateAddFunction', + 'date_sub' => 'Doctrine\ORM\Query\AST\Functions\DateSubFunction', ); /** @@ -231,7 +234,7 @@ class Parser * If they match, updates the lookahead token; otherwise raises a syntax * error. * - * @param int|string token type or value + * @param int token type * @return void * @throws QueryException If the tokens dont match. */ @@ -1328,6 +1331,10 @@ class Parser $token = $this->_lexer->lookahead; $identVariable = $this->IdentificationVariable(); + if (!isset($this->_queryComponents[$identVariable])) { + $this->semanticalError('Cannot group by undefined identification variable.'); + } + return $identVariable; } @@ -1637,7 +1644,7 @@ class Parser return $this->StateFieldPathExpression(); } else if ($lookahead == Lexer::T_INTEGER || $lookahead == Lexer::T_FLOAT) { return $this->SimpleArithmeticExpression(); - } else if ($this->_isFunction()) { + } else if ($this->_isFunction() || $this->_isAggregateFunction($this->_lexer->lookahead['type'])) { // We may be in an ArithmeticExpression (find the matching ")" and inspect for Math operator) $this->_lexer->peek(); // "(" $peek = $this->_peekBeyondClosingParenthesis(); @@ -1645,8 +1652,12 @@ class Parser if ($this->_isMathOperator($peek)) { return $this->SimpleArithmeticExpression(); } - - return $this->FunctionDeclaration(); + + if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { + return $this->AggregateExpression(); + } else { + return $this->FunctionDeclaration(); + } } else if ($lookahead == Lexer::T_STRING) { return $this->StringPrimary(); } else if ($lookahead == Lexer::T_INPUT_PARAMETER) { @@ -1721,7 +1732,8 @@ class Parser $expression = $this->PartialObjectExpression(); $identVariable = $expression->identificationVariable; } else if ($this->_lexer->lookahead['type'] == Lexer::T_INTEGER || - $this->_lexer->lookahead['type'] == Lexer::T_FLOAT) { + $this->_lexer->lookahead['type'] == Lexer::T_FLOAT || + $this->_lexer->lookahead['type'] == Lexer::T_STRING) { // Shortcut: ScalarExpression => SimpleArithmeticExpression $expression = $this->SimpleArithmeticExpression(); } else { @@ -1790,15 +1802,8 @@ class Parser } $this->_lexer->peek(); - $beyond = $this->_peekBeyondClosingParenthesis(); - if ($this->_isMathOperator($beyond)) { - $expression = $this->ScalarExpression(); - } else if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { - $expression = $this->AggregateExpression(); - } else { - $expression = $this->FunctionDeclaration(); - } + $expression = $this->ScalarExpression(); $expr = new AST\SimpleSelectExpression($expression); @@ -2303,7 +2308,8 @@ class Parser if ($peek['value'] == '.') { return $this->StateFieldPathExpression(); } else if ($peek['value'] == '(') { - return $this->FunctionsReturningStrings(); + // do NOT directly go to FunctionsReturningString() because it doesnt check for custom functions. + return $this->FunctionDeclaration(); } else { $this->syntaxError("'.' or '('"); } diff --git a/lib/Doctrine/ORM/Query/QueryException.php b/lib/Doctrine/ORM/Query/QueryException.php index f9dfd0823..aafe1e9d7 100644 --- a/lib/Doctrine/ORM/Query/QueryException.php +++ b/lib/Doctrine/ORM/Query/QueryException.php @@ -75,8 +75,7 @@ class QueryException extends \Doctrine\ORM\ORMException public static function invalidPathExpression($pathExpr) { return new self( - "Invalid PathExpression '" . $pathExpr->identificationVariable . - "." . implode('.', $pathExpr->parts) . "'." + "Invalid PathExpression '" . $pathExpr->identificationVariable . "." . $pathExpr->field . "'." ); } diff --git a/lib/Doctrine/ORM/Query/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index 7066405df..b10b0d15d 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMapping.php +++ b/lib/Doctrine/ORM/Query/ResultSetMapping.php @@ -1,7 +1,5 @@ metaMappings[$columnName] = $fieldName; $this->columnOwnerMap[$columnName] = $alias; } -} - +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php b/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php new file mode 100644 index 000000000..ceacc9638 --- /dev/null +++ b/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php @@ -0,0 +1,100 @@ +. + */ + +namespace Doctrine\ORM\Query; + +use Doctrine\ORM\EntityManager; + +/** + * A ResultSetMappingBuilder uses the EntityManager to automatically populate entity fields + * + * @author Michael Ridgway + * @since 2.1 + */ +class ResultSetMappingBuilder extends ResultSetMapping +{ + /** + * @var EntityManager + */ + private $em; + + /** + * @param EntityManager + */ + public function __construct(EntityManager $em) + { + $this->em = $em; + } + + /** + * Adds a root entity and all of its fields to the result set. + * + * @param string $class The class name of the root entity. + * @param string $alias The unique alias to use for the root entity. + * @param array $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName) + */ + public function addRootEntityFromClassMetadata($class, $alias, $renamedColumns = array()) + { + $this->addEntityResult($class, $alias); + $classMetadata = $this->em->getClassMetadata($class); + if ($classMetadata->isInheritanceTypeSingleTable() || $classMetadata->isInheritanceTypeJoined()) { + throw new \InvalidArgumentException('ResultSetMapping builder does not currently support inheritance.'); + } + $platform = $this->em->getConnection()->getDatabasePlatform(); + foreach ($classMetadata->getColumnNames() AS $columnName) { + $propertyName = $classMetadata->getFieldName($columnName); + if (isset($renamedColumns[$columnName])) { + $columnName = $renamedColumns[$columnName]; + } + if (isset($this->fieldMappings[$columnName])) { + throw new \InvalidArgumentException("The column '$columnName' conflicts with another column in the mapper."); + } + $this->addFieldResult($alias, $platform->getSQLResultCasing($columnName), $propertyName); + } + } + + /** + * Adds a joined entity and all of its fields to the result set. + * + * @param string $class The class name of the joined entity. + * @param string $alias The unique alias to use for the joined entity. + * @param string $parentAlias The alias of the entity result that is the parent of this joined result. + * @param object $relation The association field that connects the parent entity result with the joined entity result. + * @param array $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName) + */ + public function addJoinedEntityFromClassMetadata($class, $alias, $parentAlias, $relation, $renamedColumns = array()) + { + $this->addJoinedEntityResult($class, $alias, $parentAlias, $relation); + $classMetadata = $this->em->getClassMetadata($class); + if ($classMetadata->isInheritanceTypeSingleTable() || $classMetadata->isInheritanceTypeJoined()) { + throw new \InvalidArgumentException('ResultSetMapping builder does not currently support inheritance.'); + } + $platform = $this->em->getConnection()->getDatabasePlatform(); + foreach ($classMetadata->getColumnNames() AS $columnName) { + $propertyName = $classMetadata->getFieldName($columnName); + if (isset($renamedColumns[$columnName])) { + $columnName = $renamedColumns[$columnName]; + } + if (isset($this->fieldMappings[$columnName])) { + throw new \InvalidArgumentException("The column '$columnName' conflicts with another column in the mapper."); + } + $this->addFieldResult($alias, $platform->getSQLResultCasing($columnName), $propertyName); + } + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 5be2ee287..a14987eaa 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -961,7 +961,8 @@ class SqlWalker implements TreeWalker $expr instanceof AST\SimpleArithmeticExpression || $expr instanceof AST\ArithmeticTerm || $expr instanceof AST\ArithmeticFactor || - $expr instanceof AST\ArithmeticPrimary + $expr instanceof AST\ArithmeticPrimary || + $expr instanceof AST\Literal ) { if ( ! $selectExpression->fieldIdentificationVariable) { $resultAlias = $this->_scalarResultCounter++; @@ -970,7 +971,11 @@ class SqlWalker implements TreeWalker } $columnAlias = 'sclr' . $this->_aliasCounter++; - $sql .= $this->walkSimpleArithmeticExpression($expr) . ' AS ' . $columnAlias; + if ($expr instanceof AST\Literal) { + $sql .= $this->walkLiteral($expr) . ' AS ' .$columnAlias; + } else { + $sql .= $this->walkSimpleArithmeticExpression($expr) . ' AS ' . $columnAlias; + } $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); @@ -1254,9 +1259,25 @@ class SqlWalker implements TreeWalker */ public function walkGroupByClause($groupByClause) { - return ' GROUP BY ' . implode( - ', ', array_map(array($this, 'walkGroupByItem'), $groupByClause->groupByItems) - ); + $sql = ''; + foreach ($groupByClause->groupByItems AS $groupByItem) { + if (is_string($groupByItem)) { + foreach ($this->_queryComponents[$groupByItem]['metadata']->identifier AS $idField) { + if ($sql != '') { + $sql .= ', '; + } + $groupByItem = new AST\PathExpression(AST\PathExpression::TYPE_STATE_FIELD, $groupByItem, $idField); + $groupByItem->type = AST\PathExpression::TYPE_STATE_FIELD; + $sql .= $this->walkGroupByItem($groupByItem); + } + } else { + if ($sql != '') { + $sql .= ', '; + } + $sql .= $this->walkGroupByItem($groupByItem); + } + } + return ' GROUP BY ' . $sql; } /** diff --git a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php index e18a9c56a..835835d28 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php @@ -65,7 +65,7 @@ EOT protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas) { - $output->write('ATTENTION: This operation should not be executed in an production enviroment.' . PHP_EOL . PHP_EOL); + $output->write('ATTENTION: This operation should not be executed in a production enviroment.' . PHP_EOL . PHP_EOL); if ($input->getOption('dump-sql') === true) { $sqls = $schemaTool->getCreateSchemaSql($metadatas); diff --git a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php index 82d91f4c6..f6f16cb79 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php @@ -92,7 +92,7 @@ EOT } $output->write('Database schema dropped successfully!' . PHP_EOL); } else { - $output->write('ATTENTION: This operation should not be executed in an production enviroment.' . PHP_EOL . PHP_EOL); + $output->write('ATTENTION: This operation should not be executed in a production enviroment.' . PHP_EOL . PHP_EOL); if ($isFullDatabaseDrop) { $sqls = $schemaTool->getDropDatabaseSQL(); diff --git a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php index f1a3a73cf..ed12358cd 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php @@ -86,7 +86,7 @@ EOT $schemaTool->updateSchema($metadatas, $saveMode); $output->write('Database schema updated successfully!' . PHP_EOL); } else { - $output->write('ATTENTION: This operation should not be executed in an production enviroment.' . PHP_EOL); + $output->write('ATTENTION: This operation should not be executed in a production enviroment.' . PHP_EOL); $output->write('Use the incremental update to detect changes during development and use' . PHP_EOL); $output->write('this SQL DDL to manually update your database in production.' . PHP_EOL . PHP_EOL); diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index 182dd441b..9eb1f4d2a 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -179,7 +179,7 @@ public function () $this->_isNew = !file_exists($path) || (file_exists($path) && $this->_regenerateEntityIfExists); if ( ! $this->_isNew) { - $this->_parseTokensInEntityFile($path); + $this->_parseTokensInEntityFile(file_get_contents($path)); } if ($this->_backupExisting && file_exists($path)) { @@ -400,24 +400,42 @@ public function () /** * @todo this won't work if there is a namespace in brackets and a class outside of it. - * @param string $path + * @param string $src */ - private function _parseTokensInEntityFile($path) + private function _parseTokensInEntityFile($src) { - $tokens = token_get_all(file_get_contents($path)); + $tokens = token_get_all($src); $lastSeenNamespace = ""; $lastSeenClass = false; + $inNamespace = false; + $inClass = false; for ($i = 0; $i < count($tokens); $i++) { $token = $tokens[$i]; - if ($token[0] == T_NAMESPACE) { - $lastSeenNamespace = $tokens[$i+2][1] . "\\"; - } else if ($token[0] == T_NS_SEPARATOR) { - $lastSeenNamespace .= $tokens[$i+1][1] . "\\"; - } else if ($token[0] == T_CLASS) { - $lastSeenClass = $lastSeenNamespace . $tokens[$i+2][1]; + if (in_array($token[0], array(T_WHITESPACE, T_COMMENT, T_DOC_COMMENT))) { + continue; + } + + if ($inNamespace) { + if ($token[0] == T_NS_SEPARATOR || $token[0] == T_STRING) { + $lastSeenNamespace .= $token[1]; + } else if (is_string($token) && in_array($token, array(';', '{'))) { + $inNamespace = false; + } + } + + if ($inClass) { + $inClass = false; + $lastSeenClass = $lastSeenNamespace . '\\' . $token[1]; $this->_staticReflection[$lastSeenClass]['properties'] = array(); $this->_staticReflection[$lastSeenClass]['methods'] = array(); + } + + if ($token[0] == T_NAMESPACE) { + $lastSeenNamespace = ""; + $inNamespace = true; + } else if ($token[0] == T_CLASS) { + $inClass = true; } else if ($token[0] == T_FUNCTION) { if ($tokens[$i+2][0] == T_STRING) { $this->_staticReflection[$lastSeenClass]['methods'][] = $tokens[$i+2][1]; @@ -502,7 +520,7 @@ public function () } if ($metadata->isMappedSuperclass) { - $lines[] = ' * @' . $this->_annotationsPrefix . 'MappedSupperClass'; + $lines[] = ' * @' . $this->_annotationsPrefix . 'MappedSuperClass'; } else { $lines[] = ' * @' . $this->_annotationsPrefix . 'Entity'; } diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index 1edbd38a1..0ece01500 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -592,65 +592,44 @@ class SchemaTool } /** - * + * Get SQL to drop the tables defined by the passed classes. + * * @param array $classes * @return array */ public function getDropSchemaSQL(array $classes) { + $visitor = new \Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector($this->_platform); + $schema = $this->getSchemaFromMetadata($classes); + $sm = $this->_em->getConnection()->getSchemaManager(); - - $sql = array(); - $orderedTables = array(); - - foreach ($classes AS $class) { - if ($class->isIdGeneratorSequence() && !$class->isMappedSuperclass && $class->name == $class->rootEntityName && $this->_platform->supportsSequences()) { - $sql[] = $this->_platform->getDropSequenceSQL($class->sequenceGeneratorDefinition['sequenceName']); + $fullSchema = $sm->createSchema(); + foreach ($fullSchema->getTables() AS $table) { + if (!$schema->hasTable($table->getName())) { + foreach ($table->getForeignKeys() AS $foreignKey) { + /* @var $foreignKey \Doctrine\DBAL\Schema\ForeignKeyConstraint */ + if ($schema->hasTable($foreignKey->getForeignTableName())) { + $visitor->acceptForeignKey($table, $foreignKey); + } + } + } else { + $visitor->acceptTable($table); + foreach ($table->getForeignKeys() AS $foreignKey) { + $visitor->acceptForeignKey($table, $foreignKey); + } } } - $commitOrder = $this->_getCommitOrder($classes); - $associationTables = $this->_getAssociationTables($commitOrder); - - // Drop association tables first - foreach ($associationTables as $associationTable) { - if (!in_array($associationTable, $orderedTables)) { - $orderedTables[] = $associationTable; - } - } - - // Drop tables in reverse commit order - for ($i = count($commitOrder) - 1; $i >= 0; --$i) { - $class = $commitOrder[$i]; - - if (($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName) - || $class->isMappedSuperclass) { - continue; - } - - if (!in_array($class->getTableName(), $orderedTables)) { - $orderedTables[] = $class->getTableName(); - } - } - - $dropTablesSql = array(); - foreach ($orderedTables AS $tableName) { - /* @var $sm \Doctrine\DBAL\Schema\AbstractSchemaManager */ - $foreignKeys = $sm->listTableForeignKeys($tableName); - foreach ($foreignKeys AS $foreignKey) { - $sql[] = $this->_platform->getDropForeignKeySQL($foreignKey, $tableName); - } - $dropTablesSql[] = $this->_platform->getDropTableSQL($tableName); - } - - return array_merge($sql, $dropTablesSql); + return $visitor->getQueries(); } /** * Updates the database schema of the given classes by comparing the ClassMetadata - * ins$tableNametances to the current database schema that is inspected. + * instances to the current database schema that is inspected. If $saveMode is set + * to true the command is executed in the Database, else SQL is returned. * * @param array $classes + * @param boolean $saveMode * @return void */ public function updateSchema(array $classes, $saveMode=false) @@ -666,8 +645,11 @@ class SchemaTool /** * Gets the sequence of SQL statements that need to be performed in order * to bring the given class mappings in-synch with the relational schema. + * If $saveMode is set to true the command is executed in the Database, + * else SQL is returned. * * @param array $classes The classes to consider. + * @param boolean $saveMode True for writing to DB, false for SQL string * @return array The sequence of SQL statements. */ public function getUpdateSchemaSql(array $classes, $saveMode=false) @@ -686,44 +668,4 @@ class SchemaTool return $schemaDiff->toSql($this->_platform); } } - - private function _getCommitOrder(array $classes) - { - $calc = new CommitOrderCalculator; - - // Calculate dependencies - foreach ($classes as $class) { - $calc->addClass($class); - - foreach ($class->associationMappings as $assoc) { - if ($assoc['isOwningSide']) { - $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); - - if ( ! $calc->hasClass($targetClass->name)) { - $calc->addClass($targetClass); - } - - // add dependency ($targetClass before $class) - $calc->addDependency($targetClass, $class); - } - } - } - - return $calc->getCommitOrder(); - } - - private function _getAssociationTables(array $classes) - { - $associationTables = array(); - - foreach ($classes as $class) { - foreach ($class->associationMappings as $assoc) { - if ($assoc['isOwningSide'] && $assoc['type'] == ClassMetadata::MANY_TO_MANY) { - $associationTables[] = $assoc['joinTable']['name']; - } - } - } - - return $associationTables; - } } diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index d0f5bc022..90d3117e3 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -218,6 +218,13 @@ class UnitOfWork implements PropertyChangedListener //private $_readOnlyObjects = array(); + /** + * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested. + * + * @var array + */ + private $eagerLoadingEntities = array(); + /** * Initializes a new UnitOfWork instance, bound to the given EntityManager. * @@ -399,8 +406,11 @@ class UnitOfWork implements PropertyChangedListener $actualData = array(); foreach ($class->reflFields as $name => $refProp) { $value = $refProp->getValue($entity); - if ($class->isCollectionValuedAssociation($name) && $value !== null + if (isset($class->associationMappings[$name]) + && ($class->associationMappings[$name]['type'] & ClassMetadata::TO_MANY) + && $value !== null && ! ($value instanceof PersistentCollection)) { + // If $value is not a Collection then use an ArrayCollection. if ( ! $value instanceof Collection) { $value = new ArrayCollection($value); @@ -419,7 +429,7 @@ class UnitOfWork implements PropertyChangedListener $coll->setDirty( ! $coll->isEmpty()); $class->reflFields[$name]->setValue($entity, $coll); $actualData[$name] = $coll; - } else if ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) { + } else if ( (! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField) ) { $actualData[$name] = $value; } } @@ -467,9 +477,7 @@ class UnitOfWork implements PropertyChangedListener } } else if ($isChangeTrackingNotify) { continue; - } else if (is_object($orgValue) && $orgValue !== $actualValue) { - $changeSet[$propName] = array($orgValue, $actualValue); - } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) { + } else if ($orgValue !== $actualValue) { $changeSet[$propName] = array($orgValue, $actualValue); } } @@ -507,9 +515,9 @@ class UnitOfWork implements PropertyChangedListener $class = $this->em->getClassMetadata($className); // Skip class if instances are read-only - //if ($class->isReadOnly) { - // continue; - //} + if ($class->isReadOnly) { + continue; + } // If change tracking is explicit or happens through notification, then only compute // changes on entities of that type that are explicitly marked for synchronization. @@ -1783,6 +1791,10 @@ class UnitOfWork implements PropertyChangedListener if ($this->commitOrderCalculator !== null) { $this->commitOrderCalculator->clear(); } + + if ($this->evm->hasListeners(Events::onClear)) { + $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em)); + } } /** @@ -1886,6 +1898,9 @@ class UnitOfWork implements PropertyChangedListener $class->reflFields[$field]->setValue($entity, $value); } } + + // Loading the entity right here, if its in the eager loading map get rid of it there. + unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]); // Properly initialize any unfetched associations, if partial objects are not allowed. if ( ! isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) { @@ -1900,6 +1915,7 @@ class UnitOfWork implements PropertyChangedListener if ($assoc['type'] & ClassMetadata::TO_ONE) { if ($assoc['isOwningSide']) { $associatedId = array(); + // TODO: Is this even computed right in all cases of composite keys? foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { $joinColumnValue = isset($data[$srcColumn]) ? $data[$srcColumn] : null; if ($joinColumnValue !== null) { @@ -1915,6 +1931,10 @@ class UnitOfWork implements PropertyChangedListener $class->reflFields[$field]->setValue($entity, null); $this->originalEntityData[$oid][$field] = null; } else { + if (!isset($hints['fetchMode'][$class->name][$field])) { + $hints['fetchMode'][$class->name][$field] = $assoc['fetch']; + } + // Foreign key is set // Check identity map first // FIXME: Can break easily with composite keys if join column values are in @@ -1922,16 +1942,38 @@ class UnitOfWork implements PropertyChangedListener $relatedIdHash = implode(' ', $associatedId); if (isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash])) { $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash]; + + // if this is an uninitialized proxy, we are deferring eager loads, + // this association is marked as eager fetch, and its an uninitialized proxy (wtf!) + // then we cann append this entity for eager loading! + if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER && + isset($hints['deferEagerLoad']) && + !$targetClass->isIdentifierComposite && + $newValue instanceof Proxy && + $newValue->__isInitialized__ === false) { + + $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); + } } else { if ($targetClass->subClasses) { - // If it might be a subtype, it can not be lazy + // If it might be a subtype, it can not be lazy. There isn't even + // a way to solve this with deferred eager loading, which means putting + // an entity with subclasses at a *-to-one location is really bad! (performance-wise) $newValue = $this->getEntityPersister($assoc['targetEntity']) ->loadOneToOneEntity($assoc, $entity, null, $associatedId); } else { - if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { - // TODO: Maybe it could be optimized to do an eager fetch with a JOIN inside - // the persister instead of this rather unperformant approach. - $newValue = $this->em->find($assoc['targetEntity'], $associatedId); + // Deferred eager load only works for single identifier classes + + if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER) { + if (isset($hints['deferEagerLoad']) && !$targetClass->isIdentifierComposite) { + // TODO: Is there a faster approach? + $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); + + $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); + } else { + // TODO: This is very imperformant, ignore it? + $newValue = $this->em->find($assoc['targetEntity'], $associatedId); + } } else { $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); } @@ -1945,6 +1987,11 @@ class UnitOfWork implements PropertyChangedListener } $this->originalEntityData[$oid][$field] = $newValue; $class->reflFields[$field]->setValue($entity, $newValue); + + if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) { + $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']]; + $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity); + } } } else { // Inverse side of x-to-one can never be lazy @@ -1955,10 +2002,10 @@ class UnitOfWork implements PropertyChangedListener // Inject collection $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection); $pColl->setOwner($entity, $assoc); - + $reflField = $class->reflFields[$field]; $reflField->setValue($entity, $pColl); - + if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { $this->loadCollection($pColl); $pColl->takeSnapshot(); @@ -1982,6 +2029,25 @@ class UnitOfWork implements PropertyChangedListener return $entity; } + /** + * @return void + */ + public function triggerEagerLoads() + { + if (!$this->eagerLoadingEntities) { + return; + } + + // avoid infinite recursion + $eagerLoadingEntities = $this->eagerLoadingEntities; + $this->eagerLoadingEntities = array(); + + foreach ($eagerLoadingEntities AS $entityName => $ids) { + $class = $this->em->getClassMetadata($entityName); + $this->getEntityPersister($entityName)->loadAll(array_combine($class->identifier, array(array_values($ids)))); + } + } + /** * Initializes (loads) an uninitialized persistent collection of an entity. * diff --git a/lib/vendor/doctrine-common b/lib/vendor/doctrine-common index ba63ae0f0..076a03f8f 160000 --- a/lib/vendor/doctrine-common +++ b/lib/vendor/doctrine-common @@ -1 +1 @@ -Subproject commit ba63ae0f0b6b62a2a8617f01386698730ff2b713 +Subproject commit 076a03f8f40b6e08f0ae2f4ee2678474e64b6f59 diff --git a/lib/vendor/doctrine-dbal b/lib/vendor/doctrine-dbal index 556351d9d..0a9943872 160000 --- a/lib/vendor/doctrine-dbal +++ b/lib/vendor/doctrine-dbal @@ -1 +1 @@ -Subproject commit 556351d9d6b4a33506f2c1535cccee34faa65d62 +Subproject commit 0a99438729e59bcb5262b22c06c782de4dfacbb0 diff --git a/tests/Doctrine/Tests/Models/CMS/CmsUser.php b/tests/Doctrine/Tests/Models/CMS/CmsUser.php index 57741cad1..d9ac982ff 100644 --- a/tests/Doctrine/Tests/Models/CMS/CmsUser.php +++ b/tests/Doctrine/Tests/Models/CMS/CmsUser.php @@ -7,6 +7,9 @@ use Doctrine\Common\Collections\ArrayCollection; /** * @Entity * @Table(name="cms_users") + * @NamedQueries({ + * @NamedQuery(name="all", query="SELECT u FROM __CLASS__ u") + * }) */ class CmsUser { diff --git a/tests/Doctrine/Tests/ORM/EntityManagerTest.php b/tests/Doctrine/Tests/ORM/EntityManagerTest.php index 8f9045c34..ca896ad10 100644 --- a/tests/Doctrine/Tests/ORM/EntityManagerTest.php +++ b/tests/Doctrine/Tests/ORM/EntityManagerTest.php @@ -143,4 +143,16 @@ class EntityManagerTest extends \Doctrine\Tests\OrmTestCase $this->_em->close(); $this->_em->$methodName(new \stdClass()); } + + /** + * @group DDC-1125 + */ + public function testTransactionalAcceptsReturn() + { + $return = $this->_em->transactional(function ($em) { + return 'foo'; + }); + + $this->assertEquals('foo', $return); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/AllTests.php b/tests/Doctrine/Tests/ORM/Functional/AllTests.php index 319d5bb50..4dd2b0644 100644 --- a/tests/Doctrine/Tests/ORM/Functional/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Functional/AllTests.php @@ -35,6 +35,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Functional\AdvancedDqlQueryTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToOneUnidirectionalAssociationTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToOneBidirectionalAssociationTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToOneEagerLoadingTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToManyBidirectionalAssociationTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToManyUnidirectionalAssociationTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ManyToManyBasicAssociationTest'); @@ -57,6 +58,8 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Functional\DatabaseDriverTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\PostgreSQLIdentityStrategyTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ExtraLazyCollectionTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ClearEventTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ReadOnlyTest'); $suite->addTest(Locking\AllTests::suite()); $suite->addTest(Ticket\AllTests::suite()); diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index 7a7da0c7a..b6ca444ca 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -946,4 +946,35 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertNull($this->_em->find(get_class($ph), $ph->phonenumber)->getUser()); } + + /** + * @group DDC-952 + */ + public function testManyToOneFetchModeQuery() + { + $user = new CmsUser(); + $user->username = "beberlei"; + $user->name = "Benjamin E."; + $user->status = 'active'; + + $article = new CmsArticle(); + $article->topic = "foo"; + $article->text = "bar"; + $article->user = $user; + + $this->_em->persist($article); + $this->_em->persist($user); + $this->_em->flush(); + $this->_em->clear(); + + $qc = $this->getCurrentQueryCount(); + $dql = "SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.id = ?1"; + $article = $this->_em->createQuery($dql) + ->setParameter(1, $article->id) + ->setFetchMode('Doctrine\Tests\Models\CMS\CmsArticle', 'user', \Doctrine\ORM\Mapping\ClassMetadata::FETCH_EAGER) + ->getSingleResult(); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $article->user, "It IS a proxy, ..."); + $this->assertTrue($article->user->__isInitialized__, "...but its initialized!"); + $this->assertEquals($qc+2, $this->getCurrentQueryCount()); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php index 099afeb8a..0fbff503e 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php @@ -410,4 +410,29 @@ class ClassTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase $ref = $this->_em->getReference('Doctrine\Tests\Models\Company\CompanyManager', $manager->getId()); $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $ref, "A proxy can be generated only if no subclasses exists for the requested reference."); } + + /** + * @group DDC-992 + */ + public function testGetSubClassManyToManyCollection() + { + $manager = new CompanyManager(); + $manager->setName('gblanco'); + $manager->setSalary(1234); + $manager->setTitle('Awesome!'); + $manager->setDepartment('IT'); + + $person = new CompanyPerson(); + $person->setName('friend'); + + $manager->addFriend($person); + + $this->_em->persist($manager); + $this->_em->persist($person); + $this->_em->flush(); + $this->_em->clear(); + + $manager = $this->_em->find('Doctrine\Tests\Models\Company\CompanyManager', $manager->getId()); + $this->assertEquals(1, count($manager->getFriends())); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/ClearEventTest.php b/tests/Doctrine/Tests/ORM/Functional/ClearEventTest.php new file mode 100644 index 000000000..e86edb5b7 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/ClearEventTest.php @@ -0,0 +1,40 @@ + + */ +class ClearEventTest extends \Doctrine\Tests\OrmFunctionalTestCase +{ + protected function setUp() { + parent::setUp(); + } + + public function testEventIsCalledOnClear() + { + $listener = new OnClearListener; + $this->_em->getEventManager()->addEventListener(Events::onClear, $listener); + + $this->_em->clear(); + + $this->assertTrue($listener->called); + } +} + +class OnClearListener +{ + public $called = false; + + public function onClear(OnClearEventArgs $args) + { + $this->called = true; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php b/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php index 964c57119..37ce0b6ea 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php @@ -104,9 +104,42 @@ class DatabaseDriverTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertArrayHasKey('user', $metadatas['CmsGroups']->associationMappings); } + public function testIgnoreManyToManyTableWithoutFurtherForeignKeyDetails() + { + $tableB = new \Doctrine\DBAL\Schema\Table("dbdriver_bar"); + $tableB->addColumn('id', 'integer'); + $tableB->setPrimaryKey(array('id')); + + $tableA = new \Doctrine\DBAL\Schema\Table("dbdriver_baz"); + $tableA->addColumn('id', 'integer'); + $tableA->setPrimaryKey(array('id')); + + $tableMany = new \Doctrine\DBAL\Schema\Table("dbdriver_bar_baz"); + $tableMany->addColumn('bar_id', 'integer'); + $tableMany->addColumn('baz_id', 'integer'); + $tableMany->addForeignKeyConstraint('dbdriver_bar', array('bar_id'), array('id')); + + $metadatas = $this->convertToClassMetadata(array($tableA, $tableB), array($tableMany)); + + $this->assertEquals(0, count($metadatas['DbdriverBaz']->associationMappings), "no association mappings should be detected."); + } + + protected function convertToClassMetadata(array $entityTables, array $manyTables = array()) + { + $driver = new \Doctrine\ORM\Mapping\Driver\DatabaseDriver($this->_sm); + $driver->setTables($entityTables, $manyTables); + + $metadatas = array(); + foreach ($driver->getAllClassNames() AS $className) { + $class = new ClassMetadataInfo($className); + $driver->loadMetadataForClass($className, $class); + $metadatas[$className] = $class; + } + + return $metadatas; + } /** - * * @param string $className * @return ClassMetadata */ diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index ebe2c19f2..6c620db38 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -172,10 +172,10 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $userId = $user->id; - $this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $userId); + $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $userId); $this->setExpectedException('Doctrine\ORM\OptimisticLockException'); - $this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $userId, \Doctrine\DBAL\LockMode::OPTIMISTIC); + $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $userId, \Doctrine\DBAL\LockMode::OPTIMISTIC); } /** @@ -288,5 +288,72 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertType('Doctrine\Tests\Models\CMS\CmsAddress', $address); $this->assertEquals($addressId, $address->id); } + + public function testValidNamedQueryRetrieval() + { + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + + $query = $repos->createNamedQuery('all'); + + $this->assertType('Doctrine\ORM\Query', $query); + $this->assertEquals('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u', $query->getDQL()); + } + + public function testInvalidNamedQueryRetrieval() + { + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + + $this->setExpectedException('Doctrine\ORM\Mapping\MappingException'); + + $repos->createNamedQuery('invalidNamedQuery'); + } + + /** + * @group DDC-1087 + */ + public function testIsNullCriteria() + { + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + $users = $repos->findBy(array('status' => null, 'username' => 'romanb')); + + $params = $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery]['params']; + $this->assertEquals(1, count($params), "Should only execute with one parameter."); + $this->assertEquals(array('romanb'), $params); + } + + /** + * @group DDC-1094 + */ + public function testFindByLimitOffset() + { + $this->loadFixture(); + + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + + $users1 = $repos->findBy(array(), null, 1, 0); + $users2 = $repos->findBy(array(), null, 1, 1); + + $this->assertEquals(2, count($repos->findBy(array()))); + $this->assertEquals(1, count($users1)); + $this->assertEquals(1, count($users2)); + $this->assertNotSame($users1[0], $users2[0]); + } + + /** + * @group DDC-1094 + */ + public function testFindByOrderBy() + { + $this->loadFixture(); + + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + $usersAsc = $repos->findBy(array(), array("username" => "ASC")); + $usersDesc = $repos->findBy(array(), array("username" => "DESC")); + + $this->assertEquals(2, count($usersAsc), "Pre-condition: only two users in fixture"); + $this->assertEquals(2, count($usersDesc), "Pre-condition: only two users in fixture"); + $this->assertSame($usersAsc[0], $usersDesc[1]); + $this->assertSame($usersAsc[1], $usersDesc[0]); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php index 031061b84..4bf010602 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -196,8 +196,8 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(2, count($someUsers)); $this->assertEquals(2, count($otherUsers)); - // +2 queries executed by slice, +4 are executed by EAGER fetching of User Address. - $this->assertEquals($queryCount + 2 + 4, $this->getCurrentQueryCount()); + // +2 queries executed by slice + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount(), "Slicing two parts should only execute two additional queries."); } /** diff --git a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php index 23dfa3a73..f997c6c01 100644 --- a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php @@ -3,9 +3,12 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\ORM\Query\ResultSetMapping; +use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\Tests\Models\CMS\CmsUser; use Doctrine\Tests\Models\CMS\CmsPhonenumber; use Doctrine\Tests\Models\CMS\CmsAddress; +use Doctrine\Tests\Models\Company\CompanyFixContract; +use Doctrine\Tests\Models\Company\CompanyEmployee; require_once __DIR__ . '/../../TestInit.php'; @@ -156,5 +159,111 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertSame($q, $q2); } + + public function testJoinedOneToManyNativeQueryWithRSMBuilder() + { + $user = new CmsUser; + $user->name = 'Roman'; + $user->username = 'romanb'; + $user->status = 'dev'; + + $phone = new CmsPhonenumber; + $phone->phonenumber = 424242; + + $user->addPhonenumber($phone); + + $this->_em->persist($user); + $this->_em->flush(); + + $this->_em->clear(); + + $rsm = new ResultSetMappingBuilder($this->_em); + $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addJoinedEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsPhonenumber', 'p', 'u', 'phonenumbers'); + $query = $this->_em->createNativeQuery('SELECT u.*, p.* FROM cms_users u LEFT JOIN cms_phonenumbers p ON u.id = p.user_id WHERE username = ?', $rsm); + $query->setParameter(1, 'romanb'); + + $users = $query->getResult(); + $this->assertEquals(1, count($users)); + $this->assertTrue($users[0] instanceof CmsUser); + $this->assertEquals('Roman', $users[0]->name); + $this->assertTrue($users[0]->getPhonenumbers() instanceof \Doctrine\ORM\PersistentCollection); + $this->assertTrue($users[0]->getPhonenumbers()->isInitialized()); + $this->assertEquals(1, count($users[0]->getPhonenumbers())); + $phones = $users[0]->getPhonenumbers(); + $this->assertEquals(424242, $phones[0]->phonenumber); + $this->assertTrue($phones[0]->getUser() === $users[0]); + } + + public function testJoinedOneToOneNativeQueryWithRSMBuilder() + { + $user = new CmsUser; + $user->name = 'Roman'; + $user->username = 'romanb'; + $user->status = 'dev'; + + $addr = new CmsAddress; + $addr->country = 'germany'; + $addr->zip = 10827; + $addr->city = 'Berlin'; + + + $user->setAddress($addr); + + $this->_em->persist($user); + $this->_em->flush(); + + $this->_em->clear(); + + + $rsm = new ResultSetMappingBuilder($this->_em); + $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addJoinedEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress', 'a', 'u', 'address', array('id' => 'a_id')); + + $query = $this->_em->createNativeQuery('SELECT u.*, a.*, a.id AS a_id FROM cms_users u INNER JOIN cms_addresses a ON u.id = a.user_id WHERE u.username = ?', $rsm); + $query->setParameter(1, 'romanb'); + + $users = $query->getResult(); + + $this->assertEquals(1, count($users)); + $this->assertTrue($users[0] instanceof CmsUser); + $this->assertEquals('Roman', $users[0]->name); + $this->assertTrue($users[0]->getPhonenumbers() instanceof \Doctrine\ORM\PersistentCollection); + $this->assertFalse($users[0]->getPhonenumbers()->isInitialized()); + $this->assertTrue($users[0]->getAddress() instanceof CmsAddress); + $this->assertTrue($users[0]->getAddress()->getUser() == $users[0]); + $this->assertEquals('germany', $users[0]->getAddress()->getCountry()); + $this->assertEquals(10827, $users[0]->getAddress()->getZipCode()); + $this->assertEquals('Berlin', $users[0]->getAddress()->getCity()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testRSMBuilderThrowsExceptionOnColumnConflict() + { + $rsm = new ResultSetMappingBuilder($this->_em); + $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addJoinedEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress', 'a', 'u', 'address'); + } + + /** + * @group PR-39 + */ + public function testUnknownParentAliasThrowsException() + { + $rsm = new ResultSetMappingBuilder($this->_em); + $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addJoinedEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress', 'a', 'un', 'address', array('id' => 'a_id')); + + $query = $this->_em->createNativeQuery('SELECT u.*, a.*, a.id AS a_id FROM cms_users u INNER JOIN cms_addresses a ON u.id = a.user_id WHERE u.username = ?', $rsm); + $query->setParameter(1, 'romanb'); + + $this->setExpectedException( + "Doctrine\ORM\Internal\Hydration\HydrationException", + "The parent object of entity result with alias 'a' was not found. The parent alias is 'un'." + ); + $users = $query->getResult(); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php new file mode 100644 index 000000000..044a17381 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -0,0 +1,198 @@ +_em); + try { + $schemaTool->createSchema(array( + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\Train'), + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\TrainDriver'), + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\Waggon'), + )); + } catch(\Exception $e) {} + } + + public function testEagerLoadOneToOneOwningSide() + { + $train = new Train(); + $driver = new TrainDriver("Benjamin"); + $waggon = new Waggon(); + + $train->setDriver($driver); + $train->addWaggon($waggon); + + $this->_em->persist($train); // cascades + $this->_em->flush(); + $this->_em->clear(); + + $sqlCount = count($this->_sqlLoggerStack->queries); + + $train = $this->_em->find(get_class($train), $train->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $train->driver); + $this->assertEquals("Benjamin", $train->driver->name); + + $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); + } + + public function testEagerLoadOneToOneNullOwningSide() + { + $train = new Train(); + + $this->_em->persist($train); // cascades + $this->_em->flush(); + $this->_em->clear(); + + $sqlCount = count($this->_sqlLoggerStack->queries); + + $train = $this->_em->find(get_class($train), $train->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $train->driver); + $this->assertNull($train->driver); + + $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); + } + + public function testEagerLoadOneToOneInverseSide() + { + $train = new Train(); + $driver = new TrainDriver("Benjamin"); + $train->setDriver($driver); + + $this->_em->persist($train); // cascades + $this->_em->flush(); + $this->_em->clear(); + + $sqlCount = count($this->_sqlLoggerStack->queries); + + $driver = $this->_em->find(get_class($driver), $driver->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $driver->train); + $this->assertNotNull($driver->train); + + $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); + } + + public function testEagerLoadOneToOneNullInverseSide() + { + $driver = new TrainDriver("Dagny Taggert"); + + $this->_em->persist($driver); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertNull($driver->train); + + $sqlCount = count($this->_sqlLoggerStack->queries); + + $driver = $this->_em->find(get_class($driver), $driver->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $driver->train); + $this->assertNull($driver->train); + + $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); + } + + public function testEagerLoadManyToOne() + { + $train = new Train(); + $waggon = new Waggon(); + $train->addWaggon($waggon); + + $this->_em->persist($train); // cascades + $this->_em->flush(); + $this->_em->clear(); + + $waggon = $this->_em->find(get_class($waggon), $waggon->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $waggon->train); + $this->assertNotNull($waggon->train); + } +} + +/** + * @Entity + */ +class Train +{ + /** + * @id @column(type="integer") @generatedValue + * @var int + */ + public $id; + /** + * Owning side + * @OneToOne(targetEntity="TrainDriver", inversedBy="train", fetch="EAGER", cascade={"persist"}) + */ + public $driver; + /** + * @oneToMany(targetEntity="Waggon", mappedBy="train", cascade={"persist"}) + */ + public $waggons; + + public function __construct() + { + $this->waggons = new \Doctrine\Common\Collections\ArrayCollection(); + } + + public function setDriver(TrainDriver $driver) + { + $this->driver = $driver; + $driver->setTrain($this); + } + + public function addWaggon(Waggon $w) + { + $w->setTrain($this); + $this->waggons[] = $w; + } +} + +/** + * @Entity + */ +class TrainDriver +{ + /** @Id @Column(type="integer") @GeneratedValue */ + public $id; + /** @column(type="string") */ + public $name; + /** + * Inverse side + * @OneToOne(targetEntity="Train", mappedBy="driver", fetch="EAGER") + */ + public $train; + + public function __construct($name) + { + $this->name = $name; + } + + public function setTrain(Train $t) + { + $this->train = $t; + } +} + +/** + * @Entity + */ +class Waggon +{ + /** @id @generatedValue @column(type="integer") */ + public $id; + /** @ManyToOne(targetEntity="Train", inversedBy="waggons", fetch="EAGER") */ + public $train; + + public function setTrain($train) + { + $this->train = $train; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryDqlFunctionTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryDqlFunctionTest.php index 09ddaf0a3..42971e6b0 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryDqlFunctionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryDqlFunctionTest.php @@ -268,7 +268,49 @@ class QueryDqlFunctionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals('Guilherme B.Complaint Department', $arg[2]['namedep']); $this->assertEquals('Benjamin E.HR', $arg[3]['namedep']); } - + + /** + * @group DDC-1014 + */ + public function testDateDiff() + { + $arg = $this->_em->createQuery("SELECT DATE_DIFF(CURRENT_TIMESTAMP(), '2011-01-01') AS diff FROM Doctrine\Tests\Models\Company\CompanyManager m") + ->getARrayResult(); + + $this->assertTrue($arg[0]['diff'] > 0); + } + + /** + * @group DDC-1014 + */ + public function testDateAdd() + { + $arg = $this->_em->createQuery("SELECT DATE_ADD(CURRENT_TIMESTAMP(), 10, 'day') AS add FROM Doctrine\Tests\Models\Company\CompanyManager m") + ->getArrayResult(); + + $this->assertTrue(strtotime($arg[0]['add']) > 0); + + $arg = $this->_em->createQuery("SELECT DATE_ADD(CURRENT_TIMESTAMP(), 10, 'month') AS add FROM Doctrine\Tests\Models\Company\CompanyManager m") + ->getArrayResult(); + + $this->assertTrue(strtotime($arg[0]['add']) > 0); + } + + /** + * @group DDC-1014 + */ + public function testDateSub() + { + $arg = $this->_em->createQuery("SELECT DATE_SUB(CURRENT_TIMESTAMP(), 10, 'day') AS add FROM Doctrine\Tests\Models\Company\CompanyManager m") + ->getArrayResult(); + + $this->assertTrue(strtotime($arg[0]['add']) > 0); + + $arg = $this->_em->createQuery("SELECT DATE_SUB(CURRENT_TIMESTAMP(), 10, 'month') AS add FROM Doctrine\Tests\Models\Company\CompanyManager m") + ->getArrayResult(); + + $this->assertTrue(strtotime($arg[0]['add']) > 0); + } protected function generateFixture() { diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php index c753960f8..02b4d267e 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php @@ -4,6 +4,8 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\CMS\CmsUser, Doctrine\Tests\Models\CMS\CmsArticle; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query; require_once __DIR__ . '/../../TestInit.php'; @@ -135,6 +137,39 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $users = $q->getResult(); } + /** + * @group DDC-1070 + */ + public function testIterateResultAsArrayAndParams() + { + $article1 = new CmsArticle; + $article1->topic = "Doctrine 2"; + $article1->text = "This is an introduction to Doctrine 2."; + + $article2 = new CmsArticle; + $article2->topic = "Symfony 2"; + $article2->text = "This is an introduction to Symfony 2."; + + $this->_em->persist($article1); + $this->_em->persist($article2); + + $this->_em->flush(); + $this->_em->clear(); + $articleId = $article1->id; + + $query = $this->_em->createQuery("select a from Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.topic = ?1"); + $articles = $query->iterate(array(1 => 'Doctrine 2'), Query::HYDRATE_ARRAY); + + $found = array(); + foreach ($articles AS $article) { + $found[] = $article; + } + $this->assertEquals(1, count($found)); + $this->assertEquals(array( + array(array('id' => $articleId, 'topic' => 'Doctrine 2', 'text' => 'This is an introduction to Doctrine 2.', 'version' => 1)) + ), $found); + } + public function testIterateResult_IterativelyBuildUpUnitOfWork() { $article1 = new CmsArticle; @@ -313,4 +348,94 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertTrue($result[0]->user instanceof \Doctrine\ORM\Proxy\Proxy); $this->assertFalse($result[0]->user->__isInitialized__); } + + /** + * @group DDC-952 + */ + public function testEnableFetchEagerMode() + { + for ($i = 0; $i < 10; $i++) { + $article = new CmsArticle; + $article->topic = "dr. dolittle"; + $article->text = "Once upon a time ..."; + $author = new CmsUser; + $author->name = "anonymous"; + $author->username = "anon".$i; + $author->status = "here"; + $article->user = $author; + $this->_em->persist($author); + $this->_em->persist($article); + } + $this->_em->flush(); + $this->_em->clear(); + + $articles = $this->_em->createQuery('select a from Doctrine\Tests\Models\CMS\CmsArticle a') + ->setFetchMode('Doctrine\Tests\Models\CMS\CmsArticle', 'user', ClassMetadata::FETCH_EAGER) + ->getResult(); + + $this->assertEquals(10, count($articles)); + foreach ($articles AS $article) { + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $article); + } + } + + /** + * @group DDC-991 + */ + public function testgetOneOrNullResult() + { + $user = new CmsUser; + $user->name = 'Guilherme'; + $user->username = 'gblanco'; + $user->status = 'developer'; + $this->_em->persist($user); + $this->_em->flush(); + $this->_em->clear(); + + $query = $this->_em->createQuery("select u from Doctrine\Tests\Models\CMS\CmsUser u where u.username = 'gblanco'"); + + $fetchedUser = $query->getOneOrNullResult(); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $fetchedUser); + $this->assertEquals('gblanco', $fetchedUser->username); + + $query = $this->_em->createQuery("select u.username from Doctrine\Tests\Models\CMS\CmsUser u where u.username = 'gblanco'"); + $fetchedUsername = $query->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR); + $this->assertEquals('gblanco', $fetchedUsername); + } + + /** + * @group DDC-991 + */ + public function testgetOneOrNullResultSeveralRows() + { + $user = new CmsUser; + $user->name = 'Guilherme'; + $user->username = 'gblanco'; + $user->status = 'developer'; + $this->_em->persist($user); + $user = new CmsUser; + $user->name = 'Roman'; + $user->username = 'romanb'; + $user->status = 'developer'; + $this->_em->persist($user); + $this->_em->flush(); + $this->_em->clear(); + + $query = $this->_em->createQuery("select u from Doctrine\Tests\Models\CMS\CmsUser u"); + + $this->setExpectedException('Doctrine\ORM\NonUniqueResultException'); + $fetchedUser = $query->getOneOrNullResult(); + } + + /** + * @group DDC-991 + */ + public function testgetOneOrNullResultNoRows() + { + $query = $this->_em->createQuery("select u from Doctrine\Tests\Models\CMS\CmsUser u"); + $this->assertNull($query->getOneOrNullResult()); + + $query = $this->_em->createQuery("select u.username from Doctrine\Tests\Models\CMS\CmsUser u where u.username = 'gblanco'"); + $this->assertNull($query->getOneOrNullResult(Query::HYDRATE_SCALAR)); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/ReadOnlyTest.php b/tests/Doctrine/Tests/ORM/Functional/ReadOnlyTest.php new file mode 100644 index 000000000..8a5819956 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/ReadOnlyTest.php @@ -0,0 +1,61 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\ReadOnlyEntity'), + )); + } + + public function testReadOnlyEntityNeverChangeTracked() + { + $readOnly = new ReadOnlyEntity("Test1", 1234); + $this->_em->persist($readOnly); + $this->_em->flush(); + + $readOnly->name = "Test2"; + $readOnly->number = 4321; + + $this->_em->flush(); + $this->_em->clear(); + + $dbReadOnly = $this->_em->find('Doctrine\Tests\ORM\Functional\ReadOnlyEntity', $readOnly->id); + $this->assertEquals("Test1", $dbReadOnly->name); + $this->assertEquals(1234, $dbReadOnly->number); + } +} + +/** + * @Entity(readOnly=true) + */ +class ReadOnlyEntity +{ + /** + * @Id @GeneratedValue @Column(type="integer") + * @var int + */ + public $id; + /** @column(type="string") */ + public $name; + /** @Column(type="integer") */ + public $number; + + public function __construct($name, $number) + { + $this->name = $name; + $this->number = $number; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php index d37442171..3e66e0b12 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php @@ -42,7 +42,7 @@ class ReferenceProxyTest extends \Doctrine\Tests\OrmFunctionalTestCase { $id = $this->createProduct(); - $productProxy = $this->_factory->getProxy('Doctrine\Tests\Models\ECommerce\ECommerceProduct', array('id' => $id)); + $productProxy = $this->_em->getReference('Doctrine\Tests\Models\ECommerce\ECommerceProduct', array('id' => $id)); $this->assertEquals('Doctrine Cookbook', $productProxy->getName()); } diff --git a/tests/Doctrine/Tests/ORM/Functional/SchemaTool/CompanySchemaTest.php b/tests/Doctrine/Tests/ORM/Functional/SchemaTool/CompanySchemaTest.php index 3a14056eb..797c202f6 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SchemaTool/CompanySchemaTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SchemaTool/CompanySchemaTest.php @@ -50,4 +50,19 @@ class CompanySchemaTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertFalse($table->getColumn('pricePerHour')->getNotnull()); $this->assertFalse($table->getColumn('maxPrice')->getNotnull()); } + + /** + * @group DBAL-115 + */ + public function testDropPartSchemaWithForeignKeys() + { + if (!$this->_em->getConnection()->getDatabasePlatform()->supportsForeignKeyConstraints()) { + $this->markTestSkipped("Foreign Key test"); + } + + $sql = $this->_schemaTool->getDropSchemaSQL(array( + $this->_em->getClassMetadata('Doctrine\Tests\Models\Company\CompanyManager'), + )); + $this->assertEquals(3, count($sql)); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php index ca3856679..7271f42c3 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php @@ -2,6 +2,8 @@ namespace Doctrine\Tests\ORM\Functional; +use Doctrine\ORM\Mapping\ClassMetadata; + require_once __DIR__ . '/../../TestInit.php'; class SingleTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase @@ -349,4 +351,20 @@ class SingleTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase $ref = $this->_em->getReference('Doctrine\Tests\Models\Company\CompanyFixContract', $this->fix->getId()); $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $ref, "A proxy can be generated only if no subclasses exists for the requested reference."); } + + /** + * @group DDC-952 + */ + public function testEagerLoadInheritanceHierachy() + { + $this->loadFullFixture(); + + $dql = 'SELECT f FROM Doctrine\Tests\Models\Company\CompanyFixContract f WHERE f.id = ?1'; + $contract = $this->_em->createQuery($dql) + ->setFetchMode('Doctrine\Tests\Models\Company\CompanyFixContract', 'salesPerson', ClassMetadata::FETCH_EAGER) + ->setParameter(1, $this->fix->getId()) + ->getSingleResult(); + + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $contract->getSalesPerson()); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php b/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php index 9ed8f0821..8b37a02ba 100644 --- a/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php @@ -33,12 +33,14 @@ class StandardEntityPersisterTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->persist($customer); $this->_em->flush(); $this->_em->clear(); + $cardId = $cart->getId(); unset($cart); $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\ECommerce\ECommerceCart'); $persister = $this->_em->getUnitOfWork()->getEntityPersister('Doctrine\Tests\Models\ECommerce\ECommerceCart'); $newCart = new ECommerceCart(); + $this->_em->getUnitOfWork()->registerManaged($newCart, array('id' => $cardId), array()); $persister->load(array('customer_id' => $customer->getId()), $newCart, $class->associationMappings['customer']); $this->assertEquals('Credit card', $newCart->getPayment()); } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1040Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1040Test.php new file mode 100644 index 000000000..21a042187 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1040Test.php @@ -0,0 +1,83 @@ +useModelSet('cms'); + parent::setUp(); + } + + public function testReuseNamedEntityParameter() + { + $user = new CmsUser(); + $user->name = "John Galt"; + $user->username = "jgalt"; + $user->status = "inactive"; + + $article = new CmsArticle(); + $article->topic = "This is John Galt speaking!"; + $article->text = "Yadda Yadda!"; + $article->setAuthor($user); + + $this->_em->persist($user); + $this->_em->persist($article); + $this->_em->flush(); + + $dql = "SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.user = :author"; + $this->_em->createQuery($dql) + ->setParameter('author', $user) + ->getResult(); + + $dql = "SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.user = :author AND a.user = :author"; + $this->_em->createQuery($dql) + ->setParameter('author', $user) + ->getResult(); + + $dql = "SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.topic = :topic AND a.user = :author AND a.user = :author AND a.text = :text"; + $farticle = $this->_em->createQuery($dql) + ->setParameter('author', $user) + ->setParameter('topic', 'This is John Galt speaking!') + ->setParameter('text', 'Yadda Yadda!') + ->getSingleResult(); + + $this->assertSame($article, $farticle); + } + + public function testUseMultiplePositionalParameters() + { + $user = new CmsUser(); + $user->name = "John Galt"; + $user->username = "jgalt"; + $user->status = "inactive"; + + $article = new CmsArticle(); + $article->topic = "This is John Galt speaking!"; + $article->text = "Yadda Yadda!"; + $article->setAuthor($user); + + $this->_em->persist($user); + $this->_em->persist($article); + $this->_em->flush(); + + $dql = "SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.topic = ?1 AND a.user = ?2 AND a.user = ?3 AND a.text = ?4"; + $farticle = $this->_em->createQuery($dql) + ->setParameter(1, 'This is John Galt speaking!') + ->setParameter(2, $user) + ->setParameter(3, $user) + ->setParameter(4, 'Yadda Yadda!') + ->getSingleResult(); + + $this->assertSame($article, $farticle); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1043Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1043Test.php new file mode 100644 index 000000000..31bd8350f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1043Test.php @@ -0,0 +1,36 @@ +useModelSet('cms'); + parent::setUp(); + } + + public function testChangeSetPlusWeirdPHPCastingIntCastingRule() + { + $user = new \Doctrine\Tests\Models\CMS\CmsUser(); + $user->name = "John Galt"; + $user->username = "jgalt"; + $user->status = "+44"; + + $this->_em->persist($user); + $this->_em->flush(); + + $user->status = "44"; + $this->_em->flush(); + $this->_em->clear(); + + $user = $this->_em->find("Doctrine\Tests\Models\CMS\CmsUser", $user->id); + $this->assertSame("44", $user->status); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1050Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1050Test.php new file mode 100644 index 000000000..82e9590c0 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1050Test.php @@ -0,0 +1,37 @@ +markTestSkipped('performance skipped'); + $this->useModelSet('cms'); + parent::setUp(); + } + + public function testPerformance() + { + for ($i = 2; $i < 10000; ++$i) { + $user = new \Doctrine\Tests\Models\CMS\CmsUser(); + $user->status = 'developer'; + $user->username = 'jwage'+$i; + $user->name = 'Jonathan'; + $this->_em->persist($user); + } + $this->_em->flush(); + $this->_em->clear(); + + $s = microtime(true); + $users = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser')->findAll(); + $e = microtime(true); + echo __FUNCTION__ . " - " . ($e - $s) . " seconds" . PHP_EOL; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1129Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1129Test.php new file mode 100644 index 000000000..c481aa395 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1129Test.php @@ -0,0 +1,46 @@ +useModelSet('cms'); + parent::setUp(); + } + + public function testVersionFieldIgnoredInChangesetComputation() + { + $article = new \Doctrine\Tests\Models\CMS\CmsArticle(); + $article->text = "I don't know."; + $article->topic = "Who is John Galt?"; + + $this->_em->persist($article); + $this->_em->flush(); + + $this->assertEquals(1, $article->version); + + $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsArticle'); + $uow = $this->_em->getUnitOfWork(); + + $uow->computeChangeSet($class, $article); + $changeSet = $uow->getEntityChangeSet($article); + $this->assertEquals(0, count($changeSet), "No changesets should be computed."); + + $article->text = "This is John Galt speaking."; + $this->_em->flush(); + + $this->assertEquals(2, $article->version); + + $uow->computeChangeSet($class, $article); + $changeSet = $uow->getEntityChangeSet($article); + $this->assertEquals(0, count($changeSet), "No changesets should be computed."); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php index d375f408c..d51bdd361 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php @@ -21,6 +21,11 @@ class DDC633Test extends \Doctrine\Tests\OrmFunctionalTestCase } } + /** + * @group DDC-633 + * @group DDC-952 + * @group DDC-914 + */ public function testOneToOneEager() { $app = new DDC633Appointment(); @@ -35,7 +40,35 @@ class DDC633Test extends \Doctrine\Tests\OrmFunctionalTestCase $eagerAppointment = $this->_em->find(__NAMESPACE__ . '\DDC633Appointment', $app->id); - $this->assertNotType('Doctrine\ORM\Proxy\Proxy', $eagerAppointment->patient); + // Eager loading of one to one leads to fetch-join + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $eagerAppointment->patient); + $this->assertTrue($this->_em->contains($eagerAppointment->patient)); + } + + /** + * @group DDC-633 + * @group DDC-952 + */ + public function testDQLDeferredEagerLoad() + { + for ($i = 0; $i < 10; $i++) { + $app = new DDC633Appointment(); + $pat = new DDC633Patient(); + $app->patient = $pat; + $pat->appointment = $app; + + $this->_em->persist($app); + $this->_em->persist($pat); + } + $this->_em->flush(); + $this->_em->clear(); + + $appointments = $this->_em->createQuery("SELECT a FROM " . __NAMESPACE__ . "\DDC633Appointment a")->getResult(); + + foreach ($appointments AS $eagerAppointment) { + $this->assertType('Doctrine\ORM\Proxy\Proxy', $eagerAppointment->patient); + $this->assertTrue($eagerAppointment->patient->__isInitialized__, "Proxy should already be initialized due to eager loading!"); + } } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC992Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC992Test.php new file mode 100644 index 000000000..36d9a392f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC992Test.php @@ -0,0 +1,147 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC992Role'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC992Parent'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC992Child'), + )); + } catch(\Exception $e) { + + } + } + + public function testIssue() + { + $role = new DDC992Role(); + $role->name = "Parent"; + $child = new DDC992Role(); + $child->name = "child"; + + $role->extendedBy[] = $child; + $child->extends[] = $role; + + $this->_em->persist($role); + $this->_em->persist($child); + $this->_em->flush(); + $this->_em->clear(); + + $child = $this->_em->getRepository(get_class($role))->find($child->roleID); + $parents = count($child->extends); + $this->assertEquals(1, $parents); + foreach ($child->extends AS $parent) { + $this->assertEquals($role->getRoleID(), $parent->getRoleID()); + } + } + + public function testOneToManyChild() + { + $parent = new DDC992Parent(); + $child = new DDC992Child(); + $child->parent = $parent; + $parent->childs[] = $child; + + $this->_em->persist($parent); + $this->_em->persist($child); + $this->_em->flush(); + $this->_em->clear(); + + $parentRepository = $this->_em->getRepository(get_class($parent)); + $childRepository = $this->_em->getRepository(get_class($child)); + + $parent = $parentRepository->find($parent->id); + $this->assertEquals(1, count($parent->childs)); + $this->assertEquals(0, count($parent->childs[0]->childs())); + + $child = $parentRepository->findOneBy(array("id" => $child->id)); + $this->assertSame($parent->childs[0], $child); + + $this->_em->clear(); + + $child = $parentRepository->find($child->id); + $this->assertEquals(0, count($child->childs)); + + $this->_em->clear(); + + $child = $childRepository->find($child->id); + $this->assertEquals(0, count($child->childs)); + } +} + +/** + * @Entity + * @InheritanceType("JOINED") + * @DiscriminatorMap({"child" = "DDC992Child", "parent" = "DDC992Parent"}) + */ +class DDC992Parent +{ + /** @Id @GeneratedValue @Column(type="integer") */ + public $id; + /** @ManyToOne(targetEntity="DDC992Parent", inversedBy="childs") */ + public $parent; + /** @OneToMany(targetEntity="DDC992Child", mappedBy="parent") */ + public $childs; +} + +/** + * @Entity + */ +class DDC992Child extends DDC992Parent +{ + public function childs() + { + return $this->childs; + } +} + +/** + * @Entity + */ +class DDC992Role +{ + public function getRoleID() + { + return $this->roleID; + } + + /** + * @Id @Column(name="roleID", type="integer") + * @GeneratedValue(strategy="AUTO") + */ + public $roleID; + /** + * @Column (name="name", type="string", length="45") + */ + public $name; + /** + * @ManyToMany (targetEntity="DDC992Role", mappedBy="extends") + */ + public $extendedBy; + /** + * @ManyToMany (targetEntity="DDC992Role", inversedBy="extendedBy") + * @JoinTable (name="RoleRelations", + * joinColumns={@JoinColumn(name="roleID", referencedColumnName="roleID")}, + * inverseJoinColumns={@JoinColumn(name="extendsRoleID", referencedColumnName="roleID")} + * ) + */ + public $extends; + + public function __construct() { + $this->extends = new ArrayCollection; + $this->extendedBy = new ArrayCollection; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/TypeTest.php b/tests/Doctrine/Tests/ORM/Functional/TypeTest.php index 8ecee7ac7..a2a738b56 100644 --- a/tests/Doctrine/Tests/ORM/Functional/TypeTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/TypeTest.php @@ -119,7 +119,7 @@ class TypeTest extends \Doctrine\Tests\OrmFunctionalTestCase $dateTimeDb = $this->_em->find('Doctrine\Tests\Models\Generic\DateTimeModel', $dateTime->id); - $this->assertInstanceOf('DateTime', $dateTime->datetime); + $this->assertInstanceOf('DateTime', $dateTimeDb->datetime); $this->assertEquals('2009-10-02 20:10:52', $dateTimeDb->datetime->format('Y-m-d H:i:s')); } diff --git a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php index f618f6b3a..8f57280df 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php @@ -30,6 +30,8 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $cm->setCustomRepositoryClass("UserRepository"); $cm->setDiscriminatorColumn(array('name' => 'disc', 'type' => 'integer')); $cm->mapOneToOne(array('fieldName' => 'phonenumbers', 'targetEntity' => 'Bar', 'mappedBy' => 'foo')); + $cm->markReadOnly(); + $cm->addNamedQuery(array('name' => 'dql', 'query' => 'foo')); $this->assertEquals(1, count($cm->associationMappings)); $serialized = serialize($cm); @@ -51,6 +53,8 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $this->assertTrue($oneOneMapping['fetch'] == ClassMetadata::FETCH_LAZY); $this->assertEquals('phonenumbers', $oneOneMapping['fieldName']); $this->assertEquals('Doctrine\Tests\Models\CMS\Bar', $oneOneMapping['targetEntity']); + $this->assertTrue($cm->isReadOnly); + $this->assertEquals(array('dql' => 'foo'), $cm->namedQueries); } public function testFieldIsNullable() @@ -390,4 +394,70 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); $cm->mapField(array('fieldName' => '')); } + + public function testRetrievalOfNamedQueries() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + + $this->assertEquals(0, count($cm->getNamedQueries())); + + $cm->addNamedQuery(array( + 'name' => 'userById', + 'query' => 'SELECT u FROM __CLASS__ u WHERE u.id = ?1' + )); + + $this->assertEquals(1, count($cm->getNamedQueries())); + } + + public function testExistanceOfNamedQuery() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + + $cm->addNamedQuery(array( + 'name' => 'all', + 'query' => 'SELECT u FROM __CLASS__ u' + )); + + $this->assertTrue($cm->hasNamedQuery('all')); + $this->assertFalse($cm->hasNamedQuery('userById')); + } + + public function testRetrieveOfNamedQuery() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + + $cm->addNamedQuery(array( + 'name' => 'userById', + 'query' => 'SELECT u FROM __CLASS__ u WHERE u.id = ?1' + )); + + $this->assertEquals('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = ?1', $cm->getNamedQuery('userById')); + } + + public function testNamingCollisionNamedQueryShouldThrowException() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + + $this->setExpectedException('Doctrine\ORM\Mapping\MappingException'); + + $cm->addNamedQuery(array( + 'name' => 'userById', + 'query' => 'SELECT u FROM __CLASS__ u WHERE u.id = ?1' + )); + + $cm->addNamedQuery(array( + 'name' => 'userById', + 'query' => 'SELECT u FROM __CLASS__ u WHERE u.id = ?1' + )); + } + + /** + * @group DDC-1068 + */ + public function testClassCaseSensitivity() + { + $user = new \Doctrine\Tests\Models\CMS\CmsUser(); + $cm = new ClassMetadata('DOCTRINE\TESTS\MODELS\CMS\CMSUSER'); + $this->assertEquals('Doctrine\Tests\Models\CMS\CmsUser', $cm->name); + } } diff --git a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php index 4aadffb30..2abe648ad 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php +++ b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php @@ -10,6 +10,10 @@ $metadata->setChangeTrackingPolicy(ClassMetadataInfo::CHANGETRACKING_DEFERRED_IM $metadata->addLifecycleCallback('doStuffOnPrePersist', 'prePersist'); $metadata->addLifecycleCallback('doOtherStuffOnPrePersistToo', 'prePersist'); $metadata->addLifecycleCallback('doStuffOnPostPersist', 'postPersist'); +$metadata->addNamedQuery(array( + 'name' => 'all', + 'query' => 'SELECT u FROM __CLASS__ u' +)); $metadata->mapField(array( 'id' => true, 'fieldName' => 'id', diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml index 948430c24..c066cbef1 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml @@ -22,6 +22,10 @@ + + + + diff --git a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml index b541c8877..a787a93c9 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml +++ b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml @@ -1,6 +1,8 @@ Doctrine\Tests\ORM\Mapping\User: type: entity table: cms_users + namedQueries: + all: SELECT u FROM __CLASS__ u id: id: type: integer diff --git a/tests/Doctrine/Tests/ORM/Performance/InheritancePersisterPerformanceTest.php b/tests/Doctrine/Tests/ORM/Performance/InheritancePersisterPerformanceTest.php new file mode 100644 index 000000000..01113f21b --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Performance/InheritancePersisterPerformanceTest.php @@ -0,0 +1,63 @@ +useModelSet('company'); + parent::setUp(); + } + + public function testCompanyContract() + { + $person = new \Doctrine\Tests\Models\Company\CompanyEmployee(); + $person->setName('Poor Sales Guy'); + $person->setDepartment('Sales'); + $person->setSalary(100); + $this->_em->persist($person); + + for ($i = 0; $i < 33; $i++) { + $fix = new \Doctrine\Tests\Models\Company\CompanyFixContract(); + $fix->setFixPrice(1000); + $fix->setSalesPerson($person); + $fix->markCompleted(); + $this->_em->persist($fix); + + $flex = new \Doctrine\Tests\Models\Company\CompanyFlexContract(); + $flex->setSalesPerson($person); + $flex->setHoursWorked(100); + $flex->setPricePerHour(100); + $flex->markCompleted(); + $this->_em->persist($flex); + + $ultra = new \Doctrine\Tests\Models\Company\CompanyFlexUltraContract(); + $ultra->setSalesPerson($person); + $ultra->setHoursWorked(150); + $ultra->setPricePerHour(150); + $ultra->setMaxPrice(7000); + $this->_em->persist($ultra); + } + + $this->_em->flush(); + $this->_em->clear(); + + $start = microtime(true); + $contracts = $this->_em->getRepository('Doctrine\Tests\Models\Company\CompanyContract')->findAll(); + echo "99 CompanyContract: " . number_format(microtime(true) - $start, 6) . "\n"; + $this->assertEquals(99, count($contracts)); + + $this->_em->clear(); + + $start = microtime(true); + $contracts = $this->_em->getRepository('Doctrine\Tests\Models\Company\CompanyContract')->findAll(); + echo "99 CompanyContract: " . number_format(microtime(true) - $start, 6) . "\n"; + $this->assertEquals(99, count($contracts)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Performance/PersisterPerformanceTest.php b/tests/Doctrine/Tests/ORM/Performance/PersisterPerformanceTest.php new file mode 100644 index 000000000..bbd4f445f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Performance/PersisterPerformanceTest.php @@ -0,0 +1,118 @@ +useModelSet('cms'); + parent::setUp(); + } + + public function testFindCmsArticle() + { + $author = new CmsUser(); + $author->name = "beberlei"; + $author->status = "active"; + $author->username = "beberlei"; + $this->_em->persist($author); + + $ids = array(); + for ($i = 0; $i < 100; $i++) { + $article = new CmsArticle(); + $article->text = "foo"; + $article->topic = "bar"; + $article->user = $author; + $this->_em->persist($article); + $ids[] = $article; + } + $this->_em->flush(); + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsArticle')->findAll(); + echo "100 CmsArticle findAll(): " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsArticle')->findAll(); + echo "100 CmsArticle findAll(): " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + for ($i = 0; $i < 100; $i++) { + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsArticle')->find($ids[$i]->id); + } + echo "100 CmsArticle find(): " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + for ($i = 0; $i < 100; $i++) { + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsArticle')->find($ids[$i]->id); + } + echo "100 CmsArticle find(): " . number_format(microtime(true) - $start, 6) . "\n"; + } + + public function testFindCmsGroup() + { + for ($i = 0; $i < 100; $i++) { + $group = new CmsGroup(); + $group->name = "foo" . $i; + $this->_em->persist($group); + } + $this->_em->flush(); + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsGroup')->findAll(); + echo "100 CmsGroup: " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsGroup')->findAll(); + echo "100 CmsGroup: " . number_format(microtime(true) - $start, 6) . "\n"; + } + + public function testFindCmsUser() + { + for ($i = 0; $i < 100; $i++) { + $user = new CmsUser(); + $user->name = "beberlei"; + $user->status = "active"; + $user->username = "beberlei".$i; + $this->_em->persist($user); + } + + $this->_em->flush(); + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser')->findAll(); + echo "100 CmsUser: " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser')->findAll(); + echo "100 CmsUser: " . number_format(microtime(true) - $start, 6) . "\n"; + } +} + + + diff --git a/tests/Doctrine/Tests/ORM/Performance/UnitOfWorkPerformanceTest.php b/tests/Doctrine/Tests/ORM/Performance/UnitOfWorkPerformanceTest.php new file mode 100644 index 000000000..28d9ea6d3 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Performance/UnitOfWorkPerformanceTest.php @@ -0,0 +1,50 @@ +useModelSet('cms'); + parent::setUp(); + } + + public function testComputeChanges() + { + $n = 100; + + $users = array(); + for ($i=1; $i<=$n; ++$i) { + $user = new CmsUser; + $user->status = 'user'; + $user->username = 'user' . $i; + $user->name = 'Mr.Smith-' . $i; + $this->_em->persist($user); + $users[] = $user; + } + $this->_em->flush(); + + + foreach ($users AS $user) { + $user->status = 'other'; + $user->username = $user->username . '++'; + $user->name = str_replace('Mr.', 'Mrs.', $user->name); + } + + $s = microtime(true); + $this->_em->flush(); + $e = microtime(true); + + echo ' Compute ChangeSet '.$n.' objects in ' . ($e - $s) . ' seconds' . PHP_EOL; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index fae0d7350..2733d80f7 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -251,6 +251,23 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase $this->assertValidDQL("SELECT (SELECT (SUM(u.id) / COUNT(u.id)) FROM Doctrine\Tests\Models\CMS\CmsUser u2) value FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.name = 'jon'"); } + /** + * @group DDC-1079 + */ + public function testSelectLiteralInSubselect() + { + $this->assertValidDQL('SELECT (SELECT 1 FROM Doctrine\Tests\Models\CMS\CmsUser u2) value FROM Doctrine\Tests\Models\CMS\CmsUser u'); + $this->assertValidDQL('SELECT (SELECT 0 FROM Doctrine\Tests\Models\CMS\CmsUser u2) value FROM Doctrine\Tests\Models\CMS\CmsUser u'); + } + + /** + * @group DDC-1077 + */ + public function testConstantValueInSelect() + { + $this->assertValidDQL("SELECT u.name, 'foo' AS bar FROM Doctrine\Tests\Models\CMS\CmsUser u", true); + } + public function testDuplicateAliasInSubselectPart() { $this->assertInvalidDQL("SELECT (SELECT SUM(u.id) / COUNT(u.id) AS foo FROM Doctrine\Tests\Models\CMS\CmsUser u2) foo FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.name = 'jon'"); @@ -465,6 +482,16 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase $this->assertValidDQL('SELECT u, u.id + ?1 AS someNumber FROM Doctrine\Tests\Models\CMS\CmsUser u'); } + /** + * @group DDC-1091 + */ + public function testCustomFunctionsReturningStringInStringPrimary() + { + $this->_em->getConfiguration()->addCustomStringFunction('CC', 'Doctrine\ORM\Query\AST\Functions\ConcatFunction'); + + $this->assertValidDQL("SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE CC('%', u.name) LIKE '%foo%'", true); + } + /** * @group DDC-505 */ @@ -496,6 +523,38 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase $this->assertInvalidDQL('SELECT g FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.groups g'); } + /** + * @group DDC-1108 + */ + public function testInputParameterSingleChar() + { + $this->assertValidDQL('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.name = :q'); + } + + /** + * @group DDC-1053 + */ + public function testGroupBy() + { + $this->assertValidDQL('SELECT g.id, count(u.id) FROM Doctrine\Tests\Models\CMS\CmsGroup g JOIN g.users u GROUP BY g.id'); + } + + /** + * @group DDC-1053 + */ + public function testGroupByIdentificationVariable() + { + $this->assertValidDQL('SELECT g, count(u.id) FROM Doctrine\Tests\Models\CMS\CmsGroup g JOIN g.users u GROUP BY g'); + } + + /** + * @group DDC-1053 + */ + public function testGroupByUnknownIdentificationVariable() + { + $this->assertInvalidDQL('SELECT g, count(u.id) FROM Doctrine\Tests\Models\CMS\CmsGroup g JOIN g.users u GROUP BY m'); + } + /** * @group DDC-117 */ diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 7b9cb4815..2d0101e03 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -42,7 +42,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase parent::assertEquals($sqlToBeConfirmed, $query->getSql()); $query->free(); } catch (\Exception $e) { - $this->fail($e->getMessage()); + $this->fail($e->getMessage() ."\n".$e->getTraceAsString()); } } @@ -170,6 +170,17 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase ); }*/ + /** + * @group DDC-1077 + */ + public function testConstantValueInSelect() + { + $this->assertSqlGeneration( + "SELECT u.name, 'foo' AS bar FROM Doctrine\Tests\Models\CMS\CmsUser u", + "SELECT c0_.name AS name0, 'foo' AS sclr1 FROM cms_users c0_" + ); + } + public function testSupportsOrderByWithAscAsDefault() { $this->assertSqlGeneration( @@ -851,6 +862,28 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase 'SELECT f0_.id AS id0, f0_.extension AS extension1, f0_.name AS name2 FROM "file" f0_ INNER JOIN Directory d1_ ON f0_.parentDirectory_id = d1_.id WHERE f0_.id = ?' ); } + + /** + * @group DDC-1053 + */ + public function testGroupBy() + { + $this->assertSqlGeneration( + 'SELECT g.id, count(u.id) FROM Doctrine\Tests\Models\CMS\CmsGroup g JOIN g.users u GROUP BY g.id', + 'SELECT c0_.id AS id0, count(c1_.id) AS sclr1 FROM cms_groups c0_ INNER JOIN cms_users_groups c2_ ON c0_.id = c2_.group_id INNER JOIN cms_users c1_ ON c1_.id = c2_.user_id GROUP BY c0_.id' + ); + } + + /** + * @group DDC-1053 + */ + public function testGroupByIdentificationVariable() + { + $this->assertSqlGeneration( + 'SELECT g, count(u.id) FROM Doctrine\Tests\Models\CMS\CmsGroup g JOIN g.users u GROUP BY g', + 'SELECT c0_.id AS id0, c0_.name AS name1, count(c1_.id) AS sclr2 FROM cms_groups c0_ INNER JOIN cms_users_groups c2_ ON c0_.id = c2_.group_id INNER JOIN cms_users c1_ ON c1_.id = c2_.user_id GROUP BY c0_.id' + ); + } } diff --git a/tests/Doctrine/Tests/ORM/Tools/EntityGeneratorTest.php b/tests/Doctrine/Tests/ORM/Tools/EntityGeneratorTest.php index c8960f6a7..5a9c5d7eb 100644 --- a/tests/Doctrine/Tests/ORM/Tools/EntityGeneratorTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/EntityGeneratorTest.php @@ -200,6 +200,54 @@ class EntityGeneratorTest extends \Doctrine\Tests\OrmTestCase $this->assertEquals($cm->idGenerator, $metadata->idGenerator); $this->assertEquals($cm->customRepositoryClassName, $metadata->customRepositoryClassName); } + + /** + * @dataProvider getParseTokensInEntityFileData + */ + public function testParseTokensInEntityFile($php, $classes) + { + $r = new \ReflectionObject($this->_generator); + $m = $r->getMethod('_parseTokensInEntityFile'); + $m->setAccessible(true); + + $p = $r->getProperty('_staticReflection'); + $p->setAccessible(true); + + $ret = $m->invoke($this->_generator, $php); + $this->assertEquals($classes, array_keys($p->getValue($this->_generator))); + } + + public function getParseTokensInEntityFileData() + { + return array( + array( + '