diff --git a/.gitignore b/.gitignore index 04f63f22d..329249d72 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ download/ lib/api/ lib/Doctrine/Common lib/Doctrine/DBAL +/.settings/ +.buildpath +.project diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..716b9b640 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +language: php + +php: + - 5.3 + - 5.4 +env: + - DB=mysql + - DB=pgsql + - DB=sqlite + +before_script: + - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'DROP DATABASE IF EXISTS doctrine_tests;' -U postgres; fi" + - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'DROP DATABASE IF EXISTS doctrine_tests_tmp;' -U postgres; fi" + - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'create database doctrine_tests;' -U postgres; fi" + - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'create database doctrine_tests_tmp;' -U postgres; fi" + - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'create database IF NOT EXISTS doctrine_tests_tmp;create database IF NOT EXISTS doctrine_tests;'; fi" + - git submodule update --init + +script: phpunit --configuration tests/travis/$DB.travis.xml \ No newline at end of file diff --git a/README.markdown b/README.markdown index a0b5f2a20..00458ca04 100644 --- a/README.markdown +++ b/README.markdown @@ -1,14 +1,18 @@ # Doctrine 2 ORM +Master: [![Build Status](https://secure.travis-ci.org/doctrine/doctrine2.png?branch=master)](http://travis-ci.org/doctrine/doctrine2) +2.1.x: [![Build Status](https://secure.travis-ci.org/doctrine/doctrine2.png?branch=2.1.x)](http://travis-ci.org/doctrine/doctrine2) + Doctrine 2 is an object-relational mapper (ORM) for PHP 5.3.2+ that provides transparent persistence for PHP objects. It sits on top of a powerful database abstraction layer (DBAL). One of its key features is the option to write database queries in a proprietary object oriented SQL dialect called Doctrine Query Language (DQL), inspired by Hibernates HQL. This provides developers with a powerful alternative to SQL that maintains flexibility without requiring unnecessary code duplication. -More resources: +## More resources: * [Website](http://www.doctrine-project.org) * [Documentation](http://www.doctrine-project.org/projects/orm/2.0/docs/reference/introduction/en) * [Issue Tracker](http://www.doctrine-project.org/jira/browse/DDC) -* [Downloads](http://github.com/doctrine/doctrine2/downloads) \ No newline at end of file +* [Downloads](http://github.com/doctrine/doctrine2/downloads) + diff --git a/composer.json b/composer.json index 080885107..8f570e200 100644 --- a/composer.json +++ b/composer.json @@ -16,5 +16,8 @@ "ext-pdo": "*", "doctrine/common": "master-dev", "doctrine/dbal": "master-dev" + }, + "autoload": { + "psr-0": { "Doctrine\\ORM": "lib/" } } } diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 0f28e71db..72eab194b 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -545,7 +545,7 @@ abstract class AbstractQuery * * @param array $params The query parameters. * @param integer $hydrationMode The hydration mode to use. - * @return IterableResult + * @return \Doctrine\ORM\Internal\Hydration\IterableResult */ public function iterate(array $params = array(), $hydrationMode = null) { diff --git a/lib/Doctrine/ORM/EntityRepository.php b/lib/Doctrine/ORM/EntityRepository.php index 9760a1c42..a4c239001 100644 --- a/lib/Doctrine/ORM/EntityRepository.php +++ b/lib/Doctrine/ORM/EntityRepository.php @@ -225,6 +225,14 @@ class EntityRepository implements ObjectRepository return $this->_entityName; } + /** + * @return string + */ + public function getClassName() + { + return $this->getEntityName(); + } + /** * @return EntityManager */ @@ -240,4 +248,4 @@ class EntityRepository implements ObjectRepository { return $this->_class; } -} \ No newline at end of file +} diff --git a/lib/Doctrine/ORM/Event/PreFlushEventArgs.php b/lib/Doctrine/ORM/Event/PreFlushEventArgs.php new file mode 100644 index 000000000..b86967a72 --- /dev/null +++ b/lib/Doctrine/ORM/Event/PreFlushEventArgs.php @@ -0,0 +1,53 @@ +. +*/ + +namespace Doctrine\ORM\Event; + +/** + * Provides event arguments for the preFlush 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 PreFlushEventArgs extends \Doctrine\Common\EventArgs +{ + /** + * @var EntityManager + */ + private $_em; + + public function __construct($em) + { + $this->_em = $em; + } + + /** + * @return EntityManager + */ + public function getEntityManager() + { + return $this->_em; + } +} diff --git a/lib/Doctrine/ORM/Events.php b/lib/Doctrine/ORM/Events.php index e8c350aa6..8af7a9b61 100644 --- a/lib/Doctrine/ORM/Events.php +++ b/lib/Doctrine/ORM/Events.php @@ -109,6 +109,13 @@ final class Events */ const loadClassMetadata = 'loadClassMetadata'; + /** + * The preFlush event occurs when the EntityManager#flush() operation is invoked, + * but before any changes to managed entites have been calculated. This event is + * always raised right after EntityManager#flush() call. + */ + const preFlush = 'preFlush'; + /** * The onFlush event occurs when the EntityManager#flush() operation is invoked, * after any changes to managed entities have been determined but before any diff --git a/lib/Doctrine/ORM/Id/AssignedGenerator.php b/lib/Doctrine/ORM/Id/AssignedGenerator.php index 90a35fa12..2e2d4f2f6 100644 --- a/lib/Doctrine/ORM/Id/AssignedGenerator.php +++ b/lib/Doctrine/ORM/Id/AssignedGenerator.php @@ -20,6 +20,7 @@ namespace Doctrine\ORM\Id; use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\ORMException; /** @@ -42,46 +43,29 @@ class AssignedGenerator extends AbstractIdGenerator */ public function generate(EntityManager $em, $entity) { - $class = $em->getClassMetadata(get_class($entity)); + $class = $em->getClassMetadata(get_class($entity)); + $idFields = $class->getIdentifierFieldNames(); $identifier = array(); - if ($class->isIdentifierComposite) { - $idFields = $class->getIdentifierFieldNames(); - foreach ($idFields as $idField) { - $value = $class->reflFields[$idField]->getValue($entity); - if (isset($value)) { - if (isset($class->associationMappings[$idField])) { - if (!$em->getUnitOfWork()->isInIdentityMap($value)) { - throw ORMException::entityMissingForeignAssignedId($entity, $value); - } - - // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced. - $identifier[$idField] = current($em->getUnitOfWork()->getEntityIdentifier($value)); - } else { - $identifier[$idField] = $value; - } - } else { - throw ORMException::entityMissingAssignedIdForField($entity, $idField); - } - } - } else { - $idField = $class->identifier[0]; + + foreach ($idFields as $idField) { $value = $class->reflFields[$idField]->getValue($entity); - if (isset($value)) { - if (isset($class->associationMappings[$idField])) { - if (!$em->getUnitOfWork()->isInIdentityMap($value)) { - throw ORMException::entityMissingForeignAssignedId($entity, $value); - } - - // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced. - $identifier[$idField] = current($em->getUnitOfWork()->getEntityIdentifier($value)); - } else { - $identifier[$idField] = $value; - } - } else { + + if ( ! isset($value)) { throw ORMException::entityMissingAssignedIdForField($entity, $idField); } - } + + if (isset($class->associationMappings[$idField])) { + if ( ! $em->getUnitOfWork()->isInIdentityMap($value)) { + throw ORMException::entityMissingForeignAssignedId($entity, $value); + } + // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced. + $value = current($em->getUnitOfWork()->getEntityIdentifier($value)); + } + + $identifier[$idField] = $value; + } + return $identifier; } } diff --git a/lib/Doctrine/ORM/Id/IdentityGenerator.php b/lib/Doctrine/ORM/Id/IdentityGenerator.php index 75da2733d..d244871f2 100644 --- a/lib/Doctrine/ORM/Id/IdentityGenerator.php +++ b/lib/Doctrine/ORM/Id/IdentityGenerator.php @@ -46,7 +46,7 @@ class IdentityGenerator extends AbstractIdGenerator */ public function generate(EntityManager $em, $entity) { - return $em->getConnection()->lastInsertId($this->_seqName); + return (int)$em->getConnection()->lastInsertId($this->_seqName); } /** diff --git a/lib/Doctrine/ORM/Id/SequenceGenerator.php b/lib/Doctrine/ORM/Id/SequenceGenerator.php index 0d564ed32..b02331e6b 100644 --- a/lib/Doctrine/ORM/Id/SequenceGenerator.php +++ b/lib/Doctrine/ORM/Id/SequenceGenerator.php @@ -46,7 +46,7 @@ class SequenceGenerator extends AbstractIdGenerator implements Serializable $this->_sequenceName = $sequenceName; $this->_allocationSize = $allocationSize; } - + /** * Generates an ID for the given entity. * @@ -59,10 +59,12 @@ class SequenceGenerator extends AbstractIdGenerator implements Serializable if ($this->_maxValue === null || $this->_nextValue == $this->_maxValue) { // Allocate new values $conn = $em->getConnection(); - $sql = $conn->getDatabasePlatform()->getSequenceNextValSQL($this->_sequenceName); - $this->_nextValue = $conn->fetchColumn($sql); - $this->_maxValue = $this->_nextValue + $this->_allocationSize; + $sql = $conn->getDatabasePlatform()->getSequenceNextValSQL($this->_sequenceName); + + $this->_nextValue = (int)$conn->fetchColumn($sql); + $this->_maxValue = $this->_nextValue + $this->_allocationSize; } + return $this->_nextValue++; } @@ -90,13 +92,14 @@ class SequenceGenerator extends AbstractIdGenerator implements Serializable { return serialize(array( 'allocationSize' => $this->_allocationSize, - 'sequenceName' => $this->_sequenceName + 'sequenceName' => $this->_sequenceName )); } public function unserialize($serialized) { $array = unserialize($serialized); + $this->_sequenceName = $array['sequenceName']; $this->_allocationSize = $array['allocationSize']; } diff --git a/lib/Doctrine/ORM/Id/TableGenerator.php b/lib/Doctrine/ORM/Id/TableGenerator.php index 5c46f8b5c..5c49344fe 100644 --- a/lib/Doctrine/ORM/Id/TableGenerator.php +++ b/lib/Doctrine/ORM/Id/TableGenerator.php @@ -50,11 +50,12 @@ class TableGenerator extends AbstractIdGenerator if ($this->_maxValue === null || $this->_nextValue == $this->_maxValue) { // Allocate new values $conn = $em->getConnection(); - if ($conn->getTransactionNestingLevel() == 0) { - + + if ($conn->getTransactionNestingLevel() === 0) { // use select for update - $sql = $conn->getDatabasePlatform()->getTableHiLoCurrentValSql($this->_tableName, $this->_sequenceName); + $sql = $conn->getDatabasePlatform()->getTableHiLoCurrentValSql($this->_tableName, $this->_sequenceName); $currentLevel = $conn->fetchColumn($sql); + if ($currentLevel != null) { $this->_nextValue = $currentLevel; $this->_maxValue = $this->_nextValue + $this->_allocationSize; @@ -74,6 +75,7 @@ class TableGenerator extends AbstractIdGenerator // or do we want to work with table locks exclusively? } } + return $this->_nextValue++; } } \ 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 5899a69ca..146dfb5c5 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php @@ -22,15 +22,17 @@ namespace Doctrine\ORM\Internal\Hydration; use PDO, Doctrine\DBAL\Connection, Doctrine\DBAL\Types\Type, - Doctrine\ORM\EntityManager; + Doctrine\ORM\EntityManager, + Doctrine\ORM\Mapping\ClassMetadata; /** * Base class for all hydrators. A hydrator is a class that provides some form * of transformation of an SQL result set into another structure. * - * @since 2.0 - * @author Konsta Vesterinen - * @author Roman Borschel + * @since 2.0 + * @author Konsta Vesterinen + * @author Roman Borschel + * @author Guilherme Blanco */ abstract class AbstractHydrator { @@ -62,9 +64,9 @@ abstract class AbstractHydrator */ public function __construct(EntityManager $em) { - $this->_em = $em; + $this->_em = $em; $this->_platform = $em->getConnection()->getDatabasePlatform(); - $this->_uow = $em->getUnitOfWork(); + $this->_uow = $em->getUnitOfWork(); } /** @@ -72,14 +74,17 @@ abstract class AbstractHydrator * * @param object $stmt * @param object $resultSetMapping + * * @return IterableResult */ public function iterate($stmt, $resultSetMapping, array $hints = array()) { - $this->_stmt = $stmt; - $this->_rsm = $resultSetMapping; + $this->_stmt = $stmt; + $this->_rsm = $resultSetMapping; $this->_hints = $hints; - $this->_prepare(); + + $this->prepare(); + return new IterableResult($this); } @@ -92,12 +97,16 @@ abstract class AbstractHydrator */ public function hydrateAll($stmt, $resultSetMapping, array $hints = array()) { - $this->_stmt = $stmt; - $this->_rsm = $resultSetMapping; + $this->_stmt = $stmt; + $this->_rsm = $resultSetMapping; $this->_hints = $hints; - $this->_prepare(); - $result = $this->_hydrateAll(); - $this->_cleanup(); + + $this->prepare(); + + $result = $this->hydrateAllData(); + + $this->cleanup(); + return $result; } @@ -110,12 +119,17 @@ abstract class AbstractHydrator public function hydrateRow() { $row = $this->_stmt->fetch(PDO::FETCH_ASSOC); + if ( ! $row) { - $this->_cleanup(); + $this->cleanup(); + return false; } + $result = array(); - $this->_hydrateRow($row, $this->_cache, $result); + + $this->hydrateRowData($row, $this->_cache, $result); + return $result; } @@ -123,16 +137,17 @@ abstract class AbstractHydrator * Excutes one-time preparation tasks, once each time hydration is started * through {@link hydrateAll} or {@link iterate()}. */ - protected function _prepare() + protected function prepare() {} /** * Excutes one-time cleanup tasks at the end of a hydration that was initiated * through {@link hydrateAll} or {@link iterate()}. */ - protected function _cleanup() + protected function cleanup() { $this->_rsm = null; + $this->_stmt->closeCursor(); $this->_stmt = null; } @@ -146,23 +161,24 @@ abstract class AbstractHydrator * @param array $cache The cache to use. * @param mixed $result The result to fill. */ - protected function _hydrateRow(array $data, array &$cache, array &$result) + protected function hydrateRowData(array $data, array &$cache, array &$result) { - throw new HydrationException("_hydrateRow() not implemented by this hydrator."); + throw new HydrationException("hydrateRowData() not implemented by this hydrator."); } /** * Hydrates all rows from the current statement instance at once. */ - abstract protected function _hydrateAll(); + abstract protected function hydrateAllData(); /** * Processes a row of the result set. + * * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY). - * Puts the elements of a result row into a new array, grouped by the class + * Puts the elements of a result row into a new array, grouped by the dql alias * they belong to. The column names in the result set are mapped to their * field names during this procedure as well as any necessary conversions on - * the values applied. + * the values applied. Scalar values are kept in a specfic key 'scalars'. * * @param array $data SQL Result Row * @param array &$cache Cache for column to field result information @@ -172,40 +188,51 @@ abstract class AbstractHydrator * @return array An array with all the fields (name => value) of the data row, * grouped by their component alias. */ - protected function _gatherRowData(array $data, array &$cache, array &$id, array &$nonemptyComponents) + protected function gatherRowData(array $data, array &$cache, array &$id, array &$nonemptyComponents) { $rowData = array(); foreach ($data as $key => $value) { // Parse each column name only once. Cache the results. if ( ! isset($cache[$key])) { - if (isset($this->_rsm->scalarMappings[$key])) { - $cache[$key]['fieldName'] = $this->_rsm->scalarMappings[$key]; - $cache[$key]['isScalar'] = true; - } else if (isset($this->_rsm->fieldMappings[$key])) { - $fieldName = $this->_rsm->fieldMappings[$key]; - $classMetadata = $this->_em->getClassMetadata($this->_rsm->declaringClasses[$key]); - $cache[$key]['fieldName'] = $fieldName; - $cache[$key]['type'] = Type::getType($classMetadata->fieldMappings[$fieldName]['type']); - $cache[$key]['isIdentifier'] = $classMetadata->isIdentifier($fieldName); - $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; - } else if (!isset($this->_rsm->metaMappings[$key])) { - // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2 - // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping. - continue; - } else { - // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns). - $fieldName = $this->_rsm->metaMappings[$key]; - $cache[$key]['isMetaColumn'] = true; - $cache[$key]['fieldName'] = $fieldName; - $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; - $classMetadata = $this->_em->getClassMetadata($this->_rsm->aliasMap[$cache[$key]['dqlAlias']]); - $cache[$key]['isIdentifier'] = isset($this->_rsm->isIdentifierColumn[$cache[$key]['dqlAlias']][$key]); + switch (true) { + // NOTE: Most of the times it's a field mapping, so keep it first!!! + case (isset($this->_rsm->fieldMappings[$key])): + $fieldName = $this->_rsm->fieldMappings[$key]; + $classMetadata = $this->_em->getClassMetadata($this->_rsm->declaringClasses[$key]); + + $cache[$key]['fieldName'] = $fieldName; + $cache[$key]['type'] = Type::getType($classMetadata->fieldMappings[$fieldName]['type']); + $cache[$key]['isIdentifier'] = $classMetadata->isIdentifier($fieldName); + $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; + break; + + case (isset($this->_rsm->scalarMappings[$key])): + $cache[$key]['fieldName'] = $this->_rsm->scalarMappings[$key]; + $cache[$key]['isScalar'] = true; + break; + + case (isset($this->_rsm->metaMappings[$key])): + // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns). + $fieldName = $this->_rsm->metaMappings[$key]; + $classMetadata = $this->_em->getClassMetadata($this->_rsm->aliasMap[$this->_rsm->columnOwnerMap[$key]]); + + $cache[$key]['isMetaColumn'] = true; + $cache[$key]['fieldName'] = $fieldName; + $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; + $cache[$key]['isIdentifier'] = isset($this->_rsm->isIdentifierColumn[$cache[$key]['dqlAlias']][$key]); + break; + + default: + // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2 + // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping. + continue 2; } } - + if (isset($cache[$key]['isScalar'])) { $rowData['scalars'][$cache[$key]['fieldName']] = $value; + continue; } @@ -216,13 +243,14 @@ abstract class AbstractHydrator } if (isset($cache[$key]['isMetaColumn'])) { - if (!isset($rowData[$dqlAlias][$cache[$key]['fieldName']]) || $value !== null) { + if ( ! isset($rowData[$dqlAlias][$cache[$key]['fieldName']]) || $value !== null) { $rowData[$dqlAlias][$cache[$key]['fieldName']] = $value; } + continue; } - - // in an inheritance hierachy the same field could be defined several times. + + // in an inheritance hierarchy 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) { @@ -241,6 +269,7 @@ abstract class AbstractHydrator /** * Processes a row of the result set. + * * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that * simply converts column names to field names and properly converts the * values according to their types. The resulting row has the same number @@ -248,52 +277,77 @@ abstract class AbstractHydrator * * @param array $data * @param array $cache + * * @return array The processed row. */ - protected function _gatherScalarRowData(&$data, &$cache) + protected function gatherScalarRowData(&$data, &$cache) { $rowData = array(); foreach ($data as $key => $value) { // Parse each column name only once. Cache the results. if ( ! isset($cache[$key])) { - if (isset($this->_rsm->scalarMappings[$key])) { - $cache[$key]['fieldName'] = $this->_rsm->scalarMappings[$key]; - $cache[$key]['isScalar'] = true; - } else if (isset($this->_rsm->fieldMappings[$key])) { - $fieldName = $this->_rsm->fieldMappings[$key]; - $classMetadata = $this->_em->getClassMetadata($this->_rsm->declaringClasses[$key]); - $cache[$key]['fieldName'] = $fieldName; - $cache[$key]['type'] = Type::getType($classMetadata->fieldMappings[$fieldName]['type']); - $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; - } else if (!isset($this->_rsm->metaMappings[$key])) { - // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2 - // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping. - continue; - } else { - // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns). - $cache[$key]['isMetaColumn'] = true; - $cache[$key]['fieldName'] = $this->_rsm->metaMappings[$key]; - $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; + switch (true) { + // NOTE: During scalar hydration, most of the times it's a scalar mapping, keep it first!!! + case (isset($this->_rsm->scalarMappings[$key])): + $cache[$key]['fieldName'] = $this->_rsm->scalarMappings[$key]; + $cache[$key]['isScalar'] = true; + break; + + case (isset($this->_rsm->fieldMappings[$key])): + $fieldName = $this->_rsm->fieldMappings[$key]; + $classMetadata = $this->_em->getClassMetadata($this->_rsm->declaringClasses[$key]); + + $cache[$key]['fieldName'] = $fieldName; + $cache[$key]['type'] = Type::getType($classMetadata->fieldMappings[$fieldName]['type']); + $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; + break; + + case (isset($this->_rsm->metaMappings[$key])): + // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns). + $cache[$key]['isMetaColumn'] = true; + $cache[$key]['fieldName'] = $this->_rsm->metaMappings[$key]; + $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; + break; + + default: + // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2 + // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping. + continue 2; } } - + $fieldName = $cache[$key]['fieldName']; - if (isset($cache[$key]['isScalar'])) { - $rowData[$fieldName] = $value; - } else if (isset($cache[$key]['isMetaColumn'])) { - $rowData[$cache[$key]['dqlAlias'] . '_' . $fieldName] = $value; - } else { - $rowData[$cache[$key]['dqlAlias'] . '_' . $fieldName] = $cache[$key]['type'] - ->convertToPHPValue($value, $this->_platform); + switch (true) { + case (isset($cache[$key]['isScalar'])): + $rowData[$fieldName] = $value; + break; + + case (isset($cache[$key]['isMetaColumn'])): + $rowData[$cache[$key]['dqlAlias'] . '_' . $fieldName] = $value; + break; + + default: + $value = $cache[$key]['type']->convertToPHPValue($value, $this->_platform); + + $rowData[$cache[$key]['dqlAlias'] . '_' . $fieldName] = $value; } } return $rowData; } - - protected function registerManaged($class, $entity, $data) + + /** + * Register entity as managed in UnitOfWork. + * + * @param Doctrine\ORM\Mapping\ClassMetadata $class + * @param object $entity + * @param array $data + * + * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow + */ + protected function registerManaged(ClassMetadata $class, $entity, array $data) { if ($class->isIdentifierComposite) { $id = array(); @@ -311,6 +365,7 @@ abstract class AbstractHydrator $id = array($class->identifier[0] => $data[$class->identifier[0]]); } } + $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data); } } diff --git a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php index 4b1c21c6f..817e30baf 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php @@ -25,8 +25,14 @@ use PDO, Doctrine\DBAL\Connection, Doctrine\ORM\Mapping\ClassMetadata; * The ArrayHydrator produces a nested array "graph" that is often (not always) * interchangeable with the corresponding object graph for read-only access. * + * @since 2.0 * @author Roman Borschel - * @since 1.0 + * @author Guilherme Blanco + * + * @todo General behavior is "wrong" if you define an alias to selected IdentificationVariable. + * Example: SELECT u AS user FROM User u + * The result should contains an array where each array index is an array: array('user' => [User object]) + * Problem must be solved somehow by removing the isMixed in ResultSetMapping */ class ArrayHydrator extends AbstractHydrator { @@ -38,45 +44,55 @@ class ArrayHydrator extends AbstractHydrator private $_idTemplate = array(); private $_resultCounter = 0; - /** @override */ - protected function _prepare() + /** + * {@inheritdoc} + */ + protected function prepare() { - $this->_isSimpleQuery = count($this->_rsm->aliasMap) <= 1; - $this->_identifierMap = array(); + $this->_isSimpleQuery = count($this->_rsm->aliasMap) <= 1; + $this->_identifierMap = array(); $this->_resultPointers = array(); - $this->_idTemplate = array(); - $this->_resultCounter = 0; + $this->_idTemplate = array(); + $this->_resultCounter = 0; + foreach ($this->_rsm->aliasMap as $dqlAlias => $className) { - $this->_identifierMap[$dqlAlias] = array(); + $this->_identifierMap[$dqlAlias] = array(); $this->_resultPointers[$dqlAlias] = array(); - $this->_idTemplate[$dqlAlias] = ''; + $this->_idTemplate[$dqlAlias] = ''; } } - /** @override */ - protected function _hydrateAll() + /** + * {@inheritdoc} + */ + protected function hydrateAllData() { $result = array(); - $cache = array(); + $cache = array(); + while ($data = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { - $this->_hydrateRow($data, $cache, $result); + $this->hydrateRowData($data, $cache, $result); } return $result; } - /** @override */ - protected function _hydrateRow(array $data, array &$cache, array &$result) + /** + * {@inheritdoc} + */ + protected function hydrateRowData(array $row, array &$cache, array &$result) { // 1) Initialize $id = $this->_idTemplate; // initialize the id-memory $nonemptyComponents = array(); - $rowData = $this->_gatherRowData($data, $cache, $id, $nonemptyComponents); + $rowData = $this->gatherRowData($row, $cache, $id, $nonemptyComponents); // Extract scalar values. They're appended at the end. if (isset($rowData['scalars'])) { $scalars = $rowData['scalars']; + unset($rowData['scalars']); + if (empty($rowData)) { ++$this->_resultCounter; } @@ -90,7 +106,7 @@ class ArrayHydrator extends AbstractHydrator // It's a joined result $parent = $this->_rsm->parentAliasMap[$dqlAlias]; - $path = $parent . '.' . $dqlAlias; + $path = $parent . '.' . $dqlAlias; // missing parent data, skipping as RIGHT JOIN hydration is not supported. if ( ! isset($nonemptyComponents[$parent]) ) { @@ -109,39 +125,41 @@ class ArrayHydrator extends AbstractHydrator unset($this->_resultPointers[$dqlAlias]); // Ticket #1228 continue; } - + $relationAlias = $this->_rsm->relationMap[$dqlAlias]; - $relation = $this->_getClassMetadata($this->_rsm->aliasMap[$parent])->associationMappings[$relationAlias]; + $relation = $this->getClassMetadata($this->_rsm->aliasMap[$parent])->associationMappings[$relationAlias]; // Check the type of the relation (many or single-valued) if ( ! ($relation['type'] & ClassMetadata::TO_ONE)) { $oneToOne = false; + if (isset($nonemptyComponents[$dqlAlias])) { if ( ! isset($baseElement[$relationAlias])) { $baseElement[$relationAlias] = array(); } - - $indexExists = isset($this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]]); - $index = $indexExists ? $this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false; + + $indexExists = isset($this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]]); + $index = $indexExists ? $this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false; $indexIsValid = $index !== false ? isset($baseElement[$relationAlias][$index]) : false; - + if ( ! $indexExists || ! $indexIsValid) { $element = $data; if (isset($this->_rsm->indexByMap[$dqlAlias])) { - $field = $this->_rsm->indexByMap[$dqlAlias]; - $baseElement[$relationAlias][$element[$field]] = $element; + $baseElement[$relationAlias][$row[$this->_rsm->indexByMap[$dqlAlias]]] = $element; } else { $baseElement[$relationAlias][] = $element; } + end($baseElement[$relationAlias]); - $this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = - key($baseElement[$relationAlias]); + + $this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = key($baseElement[$relationAlias]); } } else if ( ! isset($baseElement[$relationAlias])) { $baseElement[$relationAlias] = array(); } } else { $oneToOne = true; + if ( ! isset($nonemptyComponents[$dqlAlias]) && ! isset($baseElement[$relationAlias])) { $baseElement[$relationAlias] = null; } else if ( ! isset($baseElement[$relationAlias])) { @@ -157,43 +175,42 @@ class ArrayHydrator extends AbstractHydrator } else { // It's a root result element - + $this->_rootAliases[$dqlAlias] = true; // Mark as root + $entityKey = $this->_rsm->entityMappings[$dqlAlias] ?: 0; // if this row has a NULL value for the root result id then make it a null result. if ( ! isset($nonemptyComponents[$dqlAlias]) ) { if ($this->_rsm->isMixed) { - $result[] = array(0 => null); + $result[] = array($entityKey => null); } else { $result[] = null; } + $resultKey = $this->_resultCounter; ++$this->_resultCounter; continue; } - + // Check for an existing element if ($this->_isSimpleQuery || ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { $element = $rowData[$dqlAlias]; - if (isset($this->_rsm->indexByMap[$dqlAlias])) { - $field = $this->_rsm->indexByMap[$dqlAlias]; - if ($this->_rsm->isMixed) { - $result[] = array($element[$field] => $element); - ++$this->_resultCounter; - } else { - $result[$element[$field]] = $element; - } - } else { - if ($this->_rsm->isMixed) { - $result[] = array($element); - ++$this->_resultCounter; - } else { - $result[] = $element; - } + if ($this->_rsm->isMixed) { + $element = array($entityKey => $element); } - end($result); - $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = key($result); + + if (isset($this->_rsm->indexByMap[$dqlAlias])) { + $resultKey = $row[$this->_rsm->indexByMap[$dqlAlias]]; + $result[$resultKey] = $element; + } else { + $resultKey = $this->_resultCounter; + $result[] = $element; + ++$this->_resultCounter; + } + + $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey; } else { $index = $this->_identifierMap[$dqlAlias][$id[$dqlAlias]]; + $resultKey = $index; /*if ($this->_rsm->isMixed) { $result[] =& $result[$index]; ++$this->_resultCounter; @@ -205,8 +222,17 @@ class ArrayHydrator extends AbstractHydrator // Append scalar values to mixed result sets if (isset($scalars)) { + if ( ! isset($resultKey) ) { + // this only ever happens when no object is fetched (scalar result only) + if (isset($this->_rsm->indexByMap['scalars'])) { + $resultKey = $row[$this->_rsm->indexByMap['scalars']]; + } else { + $resultKey = $this->_resultCounter - 1; + } + } + foreach ($scalars as $name => $value) { - $result[$this->_resultCounter - 1][$name] = $value; + $result[$resultKey][$name] = $value; } } } @@ -224,28 +250,45 @@ class ArrayHydrator extends AbstractHydrator { if ($coll === null) { unset($this->_resultPointers[$dqlAlias]); // Ticket #1228 + return; } + if ($index !== false) { $this->_resultPointers[$dqlAlias] =& $coll[$index]; + return; - } else { - if ($coll) { - if ($oneToOne) { - $this->_resultPointers[$dqlAlias] =& $coll; - } else { - end($coll); - $this->_resultPointers[$dqlAlias] =& $coll[key($coll)]; - } - } } + + if ( ! $coll) { + return; + } + + if ($oneToOne) { + $this->_resultPointers[$dqlAlias] =& $coll; + + return; + } + + end($coll); + $this->_resultPointers[$dqlAlias] =& $coll[key($coll)]; + + return; } - - private function _getClassMetadata($className) + + /** + * Retrieve ClassMetadata associated to entity class name. + * + * @param string $className + * + * @return Doctrine\ORM\Mapping\ClassMetadata + */ + private function getClassMetadata($className) { if ( ! isset($this->_ce[$className])) { $this->_ce[$className] = $this->_em->getClassMetadata($className); } + return $this->_ce[$className]; } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Internal/Hydration/HydrationException.php b/lib/Doctrine/ORM/Internal/Hydration/HydrationException.php index 886b42dec..147f6acae 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/HydrationException.php +++ b/lib/Doctrine/ORM/Internal/Hydration/HydrationException.php @@ -8,10 +8,19 @@ class HydrationException extends \Doctrine\ORM\ORMException { return new self("The result returned by the query was not unique."); } - + public static function parentObjectOfRelationNotFound($alias, $parentAlias) { return new self("The parent object of entity result with alias '$alias' was not found." . " The parent alias is '$parentAlias'."); } + + public static function emptyDiscriminatorValue($dqlAlias) + { + return new self("The DQL alias '" . $dqlAlias . "' contains an entity ". + "of an inheritance hierachy with an empty discriminator value. This means " . + "that the database contains inconsistent data with an empty " . + "discriminator value in a table row." + ); + } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 5e0c9c0be..c56b6eb22 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -29,9 +29,16 @@ use PDO, /** * The ObjectHydrator constructs an object graph out of an SQL result set. * + * @since 2.0 * @author Roman Borschel - * @since 2.0 + * @author Guilherme Blanco + * * @internal Highly performance-sensitive code. + * + * @todo General behavior is "wrong" if you define an alias to selected IdentificationVariable. + * Example: SELECT u AS user FROM User u + * The result should contains an array where each array index is an array: array('user' => [User object]) + * Problem must be solved somehow by removing the isMixed in ResultSetMapping */ class ObjectHydrator extends AbstractHydrator { @@ -53,57 +60,72 @@ class ObjectHydrator extends AbstractHydrator /** @override */ - protected function _prepare() + protected function prepare() { $this->_identifierMap = $this->_resultPointers = $this->_idTemplate = array(); + $this->_resultCounter = 0; - if (!isset($this->_hints['deferEagerLoad'])) { + + if ( ! isset($this->_hints['deferEagerLoad'])) { $this->_hints['deferEagerLoad'] = true; } foreach ($this->_rsm->aliasMap as $dqlAlias => $className) { $this->_identifierMap[$dqlAlias] = array(); - $this->_idTemplate[$dqlAlias] = ''; - $class = $this->_em->getClassMetadata($className); + $this->_idTemplate[$dqlAlias] = ''; if ( ! isset($this->_ce[$className])) { - $this->_ce[$className] = $class; + $this->_ce[$className] = $this->_em->getClassMetadata($className); } // Remember which associations are "fetch joined", so that we know where to inject // collection stubs or proxies and where not. - if (isset($this->_rsm->relationMap[$dqlAlias])) { - if ( ! isset($this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]])) { - throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $this->_rsm->parentAliasMap[$dqlAlias]); + if ( ! isset($this->_rsm->relationMap[$dqlAlias])) { + continue; + } + + if ( ! isset($this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]])) { + throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $this->_rsm->parentAliasMap[$dqlAlias]); + } + + $sourceClassName = $this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]]; + $sourceClass = $this->_getClassMetadata($sourceClassName); + $assoc = $sourceClass->associationMappings[$this->_rsm->relationMap[$dqlAlias]]; + + $this->_hints['fetched'][$sourceClassName][$assoc['fieldName']] = true; + + if ($sourceClass->subClasses) { + foreach ($sourceClass->subClasses as $sourceSubclassName) { + $this->_hints['fetched'][$sourceSubclassName][$assoc['fieldName']] = true; + } + } + + if ($assoc['type'] === ClassMetadata::MANY_TO_MANY) { + continue; + } + + // Mark any non-collection opposite sides as fetched, too. + if ($assoc['mappedBy']) { + $this->_hints['fetched'][$className][$assoc['mappedBy']] = true; + + continue; + } + + if ($assoc['inversedBy']) { + $class = $this->_ce[$className]; + $inverseAssoc = $class->associationMappings[$assoc['inversedBy']]; + + if ( ! ($inverseAssoc['type'] & ClassMetadata::TO_ONE)) { + continue; } - $sourceClassName = $this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]]; - $sourceClass = $this->_getClassMetadata($sourceClassName); - $assoc = $sourceClass->associationMappings[$this->_rsm->relationMap[$dqlAlias]]; - $this->_hints['fetched'][$sourceClassName][$assoc['fieldName']] = true; - if ($sourceClass->subClasses) { - foreach ($sourceClass->subClasses as $sourceSubclassName) { - $this->_hints['fetched'][$sourceSubclassName][$assoc['fieldName']] = true; - } - } - if ($assoc['type'] != ClassMetadata::MANY_TO_MANY) { - // Mark any non-collection opposite sides as fetched, too. - if ($assoc['mappedBy']) { - $this->_hints['fetched'][$className][$assoc['mappedBy']] = true; - } else { - if ($assoc['inversedBy']) { - $inverseAssoc = $class->associationMappings[$assoc['inversedBy']]; - if ($inverseAssoc['type'] & ClassMetadata::TO_ONE) { - $this->_hints['fetched'][$className][$inverseAssoc['fieldName']] = true; - if ($class->subClasses) { - foreach ($class->subClasses as $targetSubclassName) { - $this->_hints['fetched'][$targetSubclassName][$inverseAssoc['fieldName']] = true; - } - } - } - } + $this->_hints['fetched'][$className][$inverseAssoc['fieldName']] = true; + + if ($class->subClasses) { + foreach ($class->subClasses as $targetSubclassName) { + $this->_hints['fetched'][$targetSubclassName][$inverseAssoc['fieldName']] = true; } } } @@ -113,11 +135,12 @@ class ObjectHydrator extends AbstractHydrator /** * {@inheritdoc} */ - protected function _cleanup() + protected function cleanup() { $eagerLoad = (isset($this->_hints['deferEagerLoad'])) && $this->_hints['deferEagerLoad'] == true; - parent::_cleanup(); + parent::cleanup(); + $this->_identifierMap = $this->_initializedCollections = $this->_existingCollections = @@ -131,13 +154,13 @@ class ObjectHydrator extends AbstractHydrator /** * {@inheritdoc} */ - protected function _hydrateAll() + protected function hydrateAllData() { $result = array(); - $cache = array(); + $cache = array(); while ($row = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { - $this->_hydrateRow($row, $cache, $result); + $this->hydrateRowData($row, $cache, $result); } // Take snapshots from all newly initialized collections @@ -156,31 +179,34 @@ class ObjectHydrator extends AbstractHydrator */ private function _initRelatedCollection($entity, $class, $fieldName) { - $oid = spl_object_hash($entity); + $oid = spl_object_hash($entity); $relation = $class->associationMappings[$fieldName]; + $value = $class->reflFields[$fieldName]->getValue($entity); - $value = $class->reflFields[$fieldName]->getValue($entity); if ($value === null) { $value = new ArrayCollection; } if ( ! $value instanceof PersistentCollection) { $value = new PersistentCollection( - $this->_em, - $this->_ce[$relation['targetEntity']], - $value + $this->_em, $this->_ce[$relation['targetEntity']], $value ); $value->setOwner($entity, $relation); + $class->reflFields[$fieldName]->setValue($entity, $value); $this->_uow->setOriginalEntityProperty($oid, $fieldName, $value); + $this->_initializedCollections[$oid . $fieldName] = $value; - } else if (isset($this->_hints[Query::HINT_REFRESH]) || - isset($this->_hints['fetched'][$class->name][$fieldName]) && - ! $value->isInitialized()) { + } else if ( + isset($this->_hints[Query::HINT_REFRESH]) || + isset($this->_hints['fetched'][$class->name][$fieldName]) && + ! $value->isInitialized() + ) { // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED! $value->setDirty(false); $value->setInitialized(true); $value->unwrap()->clear(); + $this->_initializedCollections[$oid . $fieldName] = $value; } else { // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN! @@ -200,15 +226,21 @@ class ObjectHydrator extends AbstractHydrator private function _getEntity(array $data, $dqlAlias) { $className = $this->_rsm->aliasMap[$dqlAlias]; + if (isset($this->_rsm->discriminatorColumns[$dqlAlias])) { $discrColumn = $this->_rsm->metaMappings[$this->_rsm->discriminatorColumns[$dqlAlias]]; + + if ($data[$discrColumn] === "") { + throw HydrationException::emptyDiscriminatorValue($dqlAlias); + } + $className = $this->_ce[$className]->discriminatorMap[$data[$discrColumn]]; + unset($data[$discrColumn]); } if (isset($this->_hints[Query::HINT_REFRESH_ENTITY]) && isset($this->_rootAliases[$dqlAlias])) { - $class = $this->_ce[$className]; - $this->registerManaged($class, $this->_hints[Query::HINT_REFRESH_ENTITY], $data); + $this->registerManaged($this->_ce[$className], $this->_hints[Query::HINT_REFRESH_ENTITY], $data); } return $this->_uow->createEntity($className, $data, $this->_hints); @@ -218,6 +250,7 @@ class ObjectHydrator extends AbstractHydrator { // TODO: Abstract this code and UnitOfWork::createEntity() equivalent? $class = $this->_ce[$className]; + /* @var $class ClassMetadata */ if ($class->isIdentifierComposite) { $idHash = ''; @@ -249,6 +282,7 @@ class ObjectHydrator extends AbstractHydrator if ( ! isset($this->_ce[$className])) { $this->_ce[$className] = $this->_em->getClassMetadata($className); } + return $this->_ce[$className]; } @@ -273,13 +307,13 @@ class ObjectHydrator extends AbstractHydrator * @param array $cache The cache to use. * @param array $result The result array to fill. */ - protected function _hydrateRow(array $data, array &$cache, array &$result) + protected function hydrateRowData(array $row, array &$cache, array &$result) { // Initialize $id = $this->_idTemplate; // initialize the id-memory $nonemptyComponents = array(); // Split the row data into chunks of class data. - $rowData = $this->_gatherRowData($data, $cache, $id, $nonemptyComponents); + $rowData = $this->gatherRowData($row, $cache, $id, $nonemptyComponents); // Extract scalar values. They're appended at the end. if (isset($rowData['scalars'])) { @@ -352,8 +386,7 @@ class ObjectHydrator extends AbstractHydrator $element = $this->_getEntity($data, $dqlAlias); if (isset($this->_rsm->indexByMap[$dqlAlias])) { - $field = $this->_rsm->indexByMap[$dqlAlias]; - $indexValue = $this->_ce[$entityName]->reflFields[$field]->getValue($element); + $indexValue = $row[$this->_rsm->indexByMap[$dqlAlias]]; $reflFieldValue->hydrateSet($indexValue, $element); $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue; } else { @@ -380,6 +413,7 @@ class ObjectHydrator extends AbstractHydrator $reflField->setValue($parentObject, $element); $this->_uow->setOriginalEntityProperty($oid, $relationField, $element); $targetClass = $this->_ce[$relation['targetEntity']]; + if ($relation['isOwningSide']) { //TODO: Just check hints['fetched'] here? // If there is an inverse mapping on the target class its bidirectional @@ -410,14 +444,16 @@ class ObjectHydrator extends AbstractHydrator } else { // PATH C: Its a root result element $this->_rootAliases[$dqlAlias] = true; // Mark as root alias + $entityKey = $this->_rsm->entityMappings[$dqlAlias] ?: 0; // if this row has a NULL value for the root result id then make it a null result. if ( ! isset($nonemptyComponents[$dqlAlias]) ) { if ($this->_rsm->isMixed) { - $result[] = array(0 => null); + $result[] = array($entityKey => null); } else { $result[] = null; } + $resultKey = $this->_resultCounter; ++$this->_resultCounter; continue; } @@ -425,35 +461,31 @@ class ObjectHydrator extends AbstractHydrator // check for existing result from the iterations before if ( ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { $element = $this->_getEntity($rowData[$dqlAlias], $dqlAlias); + if ($this->_rsm->isMixed) { + $element = array($entityKey => $element); + } + if (isset($this->_rsm->indexByMap[$dqlAlias])) { - $field = $this->_rsm->indexByMap[$dqlAlias]; - $key = $this->_ce[$entityName]->reflFields[$field]->getValue($element); - if ($this->_rsm->isMixed) { - $element = array($key => $element); - $result[] = $element; - $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $this->_resultCounter; - ++$this->_resultCounter; - } else { - $result[$key] = $element; - $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $key; - } + $resultKey = $row[$this->_rsm->indexByMap[$dqlAlias]]; if (isset($this->_hints['collection'])) { - $this->_hints['collection']->hydrateSet($key, $element); + $this->_hints['collection']->hydrateSet($resultKey, $element); } + + $result[$resultKey] = $element; } else { - if ($this->_rsm->isMixed) { - $element = array(0 => $element); - } - $result[] = $element; - $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $this->_resultCounter; + $resultKey = $this->_resultCounter; ++$this->_resultCounter; if (isset($this->_hints['collection'])) { $this->_hints['collection']->hydrateAdd($element); } + + $result[] = $element; } + $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey; + // Update result pointer $this->_resultPointers[$dqlAlias] = $element; @@ -461,6 +493,7 @@ class ObjectHydrator extends AbstractHydrator // Update result pointer $index = $this->_identifierMap[$dqlAlias][$id[$dqlAlias]]; $this->_resultPointers[$dqlAlias] = $result[$index]; + $resultKey = $index; /*if ($this->_rsm->isMixed) { $result[] = $result[$index]; ++$this->_resultCounter; @@ -471,8 +504,16 @@ class ObjectHydrator extends AbstractHydrator // Append scalar values to mixed result sets if (isset($scalars)) { + if ( ! isset($resultKey) ) { + if (isset($this->_rsm->indexByMap['scalars'])) { + $resultKey = $row[$this->_rsm->indexByMap['scalars']]; + } else { + $resultKey = $this->_resultCounter - 1; + } + } + foreach ($scalars as $name => $value) { - $result[$this->_resultCounter - 1][$name] = $value; + $result[$resultKey][$name] = $value; } } } diff --git a/lib/Doctrine/ORM/Internal/Hydration/ScalarHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ScalarHydrator.php index f15307310..29c0d12e5 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ScalarHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ScalarHydrator.php @@ -26,25 +26,32 @@ use Doctrine\DBAL\Connection; * The created result is almost the same as a regular SQL result set, except * that column names are mapped to field names and data type conversions take place. * + * @since 2.0 * @author Roman Borschel - * @since 2.0 + * @author Guilherme Blanco */ class ScalarHydrator extends AbstractHydrator { - /** @override */ - protected function _hydrateAll() + /** + * {@inheritdoc} + */ + protected function hydrateAllData() { $result = array(); - $cache = array(); + $cache = array(); + while ($data = $this->_stmt->fetch(\PDO::FETCH_ASSOC)) { - $result[] = $this->_gatherScalarRowData($data, $cache); + $this->hydrateRowData($data, $cache, $result); } + return $result; } - /** @override */ - protected function _hydrateRow(array $data, array &$cache, array &$result) + /** + * {@inheritdoc} + */ + protected function hydrateRowData(array $data, array &$cache, array &$result) { - $result[] = $this->_gatherScalarRowData($data, $cache); + $result[] = $this->gatherScalarRowData($data, $cache); } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php index 34d8e31b1..c045c1edf 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php @@ -17,7 +17,6 @@ * . */ - namespace Doctrine\ORM\Internal\Hydration; use \PDO; @@ -32,15 +31,21 @@ class SimpleObjectHydrator extends AbstractHydrator */ private $class; + /** + * @var array + */ private $declaringClasses = array(); - protected function _hydrateAll() + /** + * {@inheritdoc} + */ + protected function hydrateAllData() { $result = array(); $cache = array(); while ($row = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { - $this->_hydrateRow($row, $cache, $result); + $this->hydrateRowData($row, $cache, $result); } $this->_em->getUnitOfWork()->triggerEagerLoads(); @@ -48,77 +53,71 @@ class SimpleObjectHydrator extends AbstractHydrator return $result; } - protected function _prepare() + /** + * {@inheritdoc} + */ + 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 (count($this->_rsm->aliasMap) !== 1) { + throw new \RuntimeException("Cannot use SimpleObjectHydrator with a ResultSetMapping that contains more than one object result."); } + if ($this->_rsm->scalarMappings) { throw new \RuntimeException("Cannot use SimpleObjectHydrator with a ResultSetMapping that contains scalar mappings."); } + + $this->class = $this->_em->getClassMetadata(reset($this->_rsm->aliasMap)); + + // We only need to add declaring classes if we have inheritance. + if ($this->class->inheritanceType === ClassMetadata::INHERITANCE_TYPE_NONE) { + return; + } + + foreach ($this->_rsm->declaringClasses AS $column => $class) { + $this->declaringClasses[$column] = $this->_em->getClassMetadata($class); + } } - protected function _hydrateRow(array $sqlResult, array &$cache, array &$result) + /** + * {@inheritdoc} + */ + protected function hydrateRowData(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 { + $entityName = $this->class->name; + $data = array(); + + // We need to find the correct entity class name if we have inheritance in resultset + if ($this->class->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { $discrColumnName = $this->_platform->getSQLResultCasing($this->class->discriminatorColumn['name']); + + if ($sqlResult[$discrColumnName] === '') { + throw HydrationException::emptyDiscriminatorValue(key($this->_rsm->aliasMap)); + } + $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]; - } + } + + foreach ($sqlResult as $column => $value) { + // Hydrate column information if not yet present + if ( ! isset($cache[$column])) { + if (($info = $this->hydrateColumnInfo($entityName, $column)) === null) { + continue; } + + $cache[$column] = $info; + } - 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; - } + // Convert field to a valid PHP value + if (isset($cache[$column]['field'])) { + $type = Type::getType($cache[$column]['class']->fieldMappings[$cache[$column]['name']]['type']); + $value = $type->convertToPHPValue($value, $this->_platform); + } + + // Prevent overwrite in case of inherit classes using same property name (See AbstractHydrator) + if (isset($cache[$column]) && ( ! isset($data[$cache[$column]['name']]) || $value !== null)) { + $data[$cache[$column]['name']] = $value; } } @@ -128,4 +127,52 @@ class SimpleObjectHydrator extends AbstractHydrator $result[] = $this->_em->getUnitOfWork()->createEntity($entityName, $data, $this->_hints); } + + /** + * Retrieve column information form ResultSetMapping. + * + * @param string $entityName + * @param string $column + * + * @return array + */ + protected function hydrateColumnInfo($entityName, $column) + { + switch (true) { + case (isset($this->_rsm->fieldMappings[$column])): + $class = isset($this->declaringClasses[$column]) + ? $this->declaringClasses[$column] + : $this->class; + + // If class is not part of the inheritance, ignore + if ( ! ($class->name === $entityName || is_subclass_of($entityName, $class->name))) { + return null; + } + + return array( + 'class' => $class, + 'name' => $this->_rsm->fieldMappings[$column], + 'field' => true, + ); + + case (isset($this->_rsm->relationMap[$column])): + $class = isset($this->_rsm->relationMap[$column]) + ? $this->_rsm->relationMap[$column] + : $this->class; + + // If class is not self referencing, ignore + if ( ! ($class === $entityName || is_subclass_of($entityName, $class))) { + return null; + } + + // TODO: Decide what to do with associations. It seems original code is incomplete. + // One solution is to load the association, but it might require extra efforts. + return array('name' => $column); + + default: + return array( + 'name' => $this->_rsm->metaMappings[$column] + ); + } + } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php index 466815521..98a45960e 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php @@ -19,30 +19,37 @@ namespace Doctrine\ORM\Internal\Hydration; -use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Connection, + Doctrine\ORM\NoResultException, + Doctrine\ORM\NonUniqueResultException; /** * Hydrator that hydrates a single scalar value from the result set. * + * @since 2.0 * @author Roman Borschel - * @since 2.0 + * @author Guilherme Blanco */ class SingleScalarHydrator extends AbstractHydrator { - /** @override */ - protected function _hydrateAll() + /** + * {@inheritdoc} + */ + protected function hydrateAllData() { - $cache = array(); - $result = $this->_stmt->fetchAll(\PDO::FETCH_ASSOC); - $num = count($result); - - if ($num == 0) { - throw new \Doctrine\ORM\NoResultException; - } else if ($num > 1 || count($result[key($result)]) > 1) { - throw new \Doctrine\ORM\NonUniqueResultException; + $data = $this->_stmt->fetchAll(\PDO::FETCH_ASSOC); + $numRows = count($data); + + if ($numRows === 0) { + throw new NoResultException(); } - $result = $this->_gatherScalarRowData($result[key($result)], $cache); + if ($numRows > 1 || count($data[key($data)]) > 1) { + throw new NonUniqueResultException(); + } + + $cache = array(); + $result = $this->gatherScalarRowData($data[key($data)], $cache); return array_shift($result); } diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index 239022b0f..19d319dbe 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -312,6 +312,10 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface if ($parent && $parent->containsForeignIdentifier) { $class->containsForeignIdentifier = true; } + + if ($parent && !empty ($parent->namedQueries)) { + $this->addInheritedNamedQueries($class, $parent); + } $class->setParentClasses($visited); @@ -428,6 +432,25 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface $subClass->addInheritedAssociationMapping($mapping); } } + + /** + * Adds inherited named queries to the subclass mapping. + * + * @since 2.2 + * @param Doctrine\ORM\Mapping\ClassMetadata $subClass + * @param Doctrine\ORM\Mapping\ClassMetadata $parentClass + */ + private function addInheritedNamedQueries(ClassMetadata $subClass, ClassMetadata $parentClass) + { + foreach ($parentClass->namedQueries as $name => $query) { + if (!isset ($subClass->namedQueries[$name])) { + $subClass->addNamedQuery(array( + 'name' => $query['name'], + 'query' => $query['query'] + )); + } + } + } /** * Completes the ID generator mapping. If "auto" is specified we choose the generator diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 25a3350d2..b8c4ef2f1 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -136,10 +136,6 @@ class ClassMetadataInfo implements ClassMetadata * Identifies a many-to-one association. */ const MANY_TO_ONE = 2; - /** - * Combined bitmask for to-one (single-valued) associations. - */ - const TO_ONE = 3; /** * Identifies a one-to-many association. */ @@ -148,6 +144,10 @@ class ClassMetadataInfo implements ClassMetadata * Identifies a many-to-many association. */ const MANY_TO_MANY = 8; + /** + * Combined bitmask for to-one (single-valued) associations. + */ + const TO_ONE = 3; /** * Combined bitmask for to-many (collection-valued) associations. */ @@ -685,7 +685,7 @@ class ClassMetadataInfo implements ClassMetadata if ( ! isset($this->namedQueries[$queryName])) { throw MappingException::queryNotFound($this->name, $queryName); } - return $this->namedQueries[$queryName]; + return $this->namedQueries[$queryName]['dql']; } /** @@ -1111,25 +1111,25 @@ class ClassMetadataInfo implements ClassMetadata */ public function getIdentifierColumnNames() { - if ($this->isIdentifierComposite) { - $columnNames = array(); - foreach ($this->identifier as $idField) { - if (isset($this->associationMappings[$idField])) { - // no composite pk as fk entity assumption: - $columnNames[] = $this->associationMappings[$idField]['joinColumns'][0]['name']; - } else { - $columnNames[] = $this->fieldMappings[$idField]['columnName']; - } + $columnNames = array(); + + foreach ($this->identifier as $idProperty) { + if (isset($this->fieldMappings[$idProperty])) { + $columnNames[] = $this->fieldMappings[$idProperty]['columnName']; + + continue; } - return $columnNames; - } else if(isset($this->fieldMappings[$this->identifier[0]])) { - return array($this->fieldMappings[$this->identifier[0]]['columnName']); - } else { - // no composite pk as fk entity assumption: - return array($this->associationMappings[$this->identifier[0]]['joinColumns'][0]['name']); + + // Association defined as Id field + $joinColumns = $this->associationMappings[$idProperty]['joinColumns']; + $assocColumnNames = array_map(function ($joinColumn) { return $joinColumn['name']; }, $joinColumns); + + $columnNames = array_merge($columnNames, $assocColumnNames); } + + return $columnNames; } - + /** * Sets the type of Id generator to use for the mapped class. */ @@ -1448,8 +1448,15 @@ class ClassMetadataInfo implements ClassMetadata 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; + + $name = $queryMapping['name']; + $query = $queryMapping['query']; + $dql = str_replace('__CLASS__', $this->name, $query); + $this->namedQueries[$name] = array( + 'name' => $name, + 'query' => $query, + 'dql' => $dql + ); } /** @@ -1504,14 +1511,16 @@ class ClassMetadataInfo implements ClassMetadata /** * Stores the association mapping. * - * @param AssociationMapping $assocMapping + * @param array $assocMapping */ protected function _storeAssociationMapping(array $assocMapping) { $sourceFieldName = $assocMapping['fieldName']; + if (isset($this->fieldMappings[$sourceFieldName]) || isset($this->associationMappings[$sourceFieldName])) { throw MappingException::duplicateFieldMapping($this->name, $sourceFieldName); } + $this->associationMappings[$sourceFieldName] = $assocMapping; } @@ -1904,6 +1913,42 @@ class ClassMetadataInfo implements ClassMetadata return $this->name; } + /** + * Gets the (possibly quoted) identifier column names for safe use in an SQL statement. + * + * @param AbstractPlatform $platform + * @return array + */ + public function getQuotedIdentifierColumnNames($platform) + { + $quotedColumnNames = array(); + + foreach ($this->identifier as $idProperty) { + if (isset($this->fieldMappings[$idProperty])) { + $quotedColumnNames[] = isset($this->fieldMappings[$idProperty]['quoted']) + ? $platform->quoteIdentifier($this->fieldMappings[$idProperty]['columnName']) + : $this->fieldMappings[$idProperty]['columnName']; + + continue; + } + + // Association defined as Id field + $joinColumns = $this->associationMappings[$idProperty]['joinColumns']; + $assocQuotedColumnNames = array_map( + function ($joinColumn) { + return isset($joinColumn['quoted']) + ? $platform->quoteIdentifier($joinColumn['name']) + : $joinColumn['name']; + }, + $joinColumns + ); + + $quotedColumnNames = array_merge($quotedColumnNames, $assocQuotedColumnNames); + } + + return $quotedColumnNames; + } + /** * Gets the (possibly quoted) column name of a mapped field for safe use * in an SQL statement. @@ -1914,7 +1959,9 @@ class ClassMetadataInfo implements ClassMetadata */ public function getQuotedColumnName($field, $platform) { - return isset($this->fieldMappings[$field]['quoted']) ? $platform->quoteIdentifier($this->fieldMappings[$field]['columnName']) : $this->fieldMappings[$field]['columnName']; + return isset($this->fieldMappings[$field]['quoted']) + ? $platform->quoteIdentifier($this->fieldMappings[$field]['columnName']) + : $this->fieldMappings[$field]['columnName']; } /** diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 36e3dcb01..d469fd66e 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -331,7 +331,7 @@ class AnnotationDriver implements Driver $mapping['inversedBy'] = $oneToOneAnnot->inversedBy; $mapping['cascade'] = $oneToOneAnnot->cascade; $mapping['orphanRemoval'] = $oneToOneAnnot->orphanRemoval; - $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $oneToOneAnnot->fetch); + $mapping['fetch'] = $this->getFetchMode($className, $oneToOneAnnot->fetch); $metadata->mapOneToOne($mapping); } else if ($oneToManyAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OneToMany')) { $mapping['mappedBy'] = $oneToManyAnnot->mappedBy; @@ -339,7 +339,7 @@ class AnnotationDriver implements Driver $mapping['cascade'] = $oneToManyAnnot->cascade; $mapping['indexBy'] = $oneToManyAnnot->indexBy; $mapping['orphanRemoval'] = $oneToManyAnnot->orphanRemoval; - $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $oneToManyAnnot->fetch); + $mapping['fetch'] = $this->getFetchMode($className, $oneToManyAnnot->fetch); if ($orderByAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OrderBy')) { $mapping['orderBy'] = $orderByAnnot->value; @@ -355,7 +355,7 @@ class AnnotationDriver implements Driver $mapping['cascade'] = $manyToOneAnnot->cascade; $mapping['inversedBy'] = $manyToOneAnnot->inversedBy; $mapping['targetEntity'] = $manyToOneAnnot->targetEntity; - $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $manyToOneAnnot->fetch); + $mapping['fetch'] = $this->getFetchMode($className, $manyToOneAnnot->fetch); $metadata->mapManyToOne($mapping); } else if ($manyToManyAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\ManyToMany')) { $joinTable = array(); @@ -395,7 +395,7 @@ class AnnotationDriver implements Driver $mapping['inversedBy'] = $manyToManyAnnot->inversedBy; $mapping['cascade'] = $manyToManyAnnot->cascade; $mapping['indexBy'] = $manyToManyAnnot->indexBy; - $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $manyToManyAnnot->fetch); + $mapping['fetch'] = $this->getFetchMode($className, $manyToManyAnnot->fetch); if ($orderByAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OrderBy')) { $mapping['orderBy'] = $orderByAnnot->value; @@ -446,6 +446,10 @@ class AnnotationDriver implements Driver if (isset($annotations['Doctrine\ORM\Mapping\PostLoad'])) { $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postLoad); } + + if (isset($annotations['Doctrine\ORM\Mapping\PreFlush'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preFlush); + } } } } @@ -536,6 +540,22 @@ class AnnotationDriver implements Driver return $classes; } + /** + * Attempts to resolve the fetch mode. + * + * @param string $className The class name + * @param string $fetchMode The fetch mode + * @return integer The fetch mode as defined in ClassMetadata + * @throws MappingException If the fetch mode is not valid + */ + private function getFetchMode($className, $fetchMode) + { + if(!defined('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode)) { + throw MappingException::invalidFetchMode($className, $fetchMode); + } + + return constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode); + } /** * Factory method for the Annotation Driver * diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index e471a0d71..7f25ecbb1 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -387,3 +387,9 @@ final class PostRemove implements Annotation {} * @Target("METHOD") */ final class PostLoad implements Annotation {} + +/** + * @Annotation + * @Target("METHOD") + */ +final class PreFlush implements Annotation {} diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index b676ca8da..2b7657763 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -89,7 +89,7 @@ class XmlDriver extends AbstractFileDriver if (isset($xmlRoot['schema'])) { $metadata->table['schema'] = (string)$xmlRoot['schema']; }*/ - + if (isset($xmlRoot['inheritance-type'])) { $inheritanceType = (string)$xmlRoot['inheritance-type']; $metadata->setInheritanceType(constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceType)); @@ -166,9 +166,12 @@ class XmlDriver extends AbstractFileDriver foreach ($xmlRoot->field as $fieldMapping) { $mapping = array( 'fieldName' => (string)$fieldMapping['name'], - 'type' => (string)$fieldMapping['type'] ); + if (isset($fieldMapping['type'])) { + $mapping['type'] = (string)$fieldMapping['type']; + } + if (isset($fieldMapping['column'])) { $mapping['columnName'] = (string)$fieldMapping['column']; } @@ -219,9 +222,12 @@ class XmlDriver extends AbstractFileDriver $mapping = array( 'id' => true, - 'fieldName' => (string)$idElement['name'], - 'type' => (string)$idElement['type'] + 'fieldName' => (string)$idElement['name'] ); + + if (isset($fieldMapping['type'])) { + $mapping['type'] = (string)$idElement['type']; + } if (isset($idElement['column'])) { $mapping['columnName'] = (string)$idElement['column']; @@ -327,6 +333,8 @@ class XmlDriver extends AbstractFileDriver if (isset($oneToManyElement['index-by'])) { $mapping['indexBy'] = (string)$oneToManyElement['index-by']; + } else if (isset($oneToManyElement->{'index-by'})) { + throw new \InvalidArgumentException(" is not a valid tag"); } $metadata->mapOneToMany($mapping); @@ -432,8 +440,10 @@ class XmlDriver extends AbstractFileDriver $mapping['orderBy'] = $orderBy; } - if (isset($manyToManyElement->{'index-by'})) { - $mapping['indexBy'] = (string)$manyToManyElement->{'index-by'}; + if (isset($manyToManyElement['index-by'])) { + $mapping['indexBy'] = (string)$manyToManyElement['index-by']; + } else if (isset($manyToManyElement->{'index-by'})) { + throw new \InvalidArgumentException(" is not a valid tag"); } $metadata->mapManyToMany($mapping); diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index a2c5402b6..5979ae73f 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -165,15 +165,14 @@ class YamlDriver extends AbstractFileDriver continue; } - if (!isset($idElement['type'])) { - throw MappingException::propertyTypeIsRequired($className, $name); - } - $mapping = array( 'id' => true, - 'fieldName' => $name, - 'type' => $idElement['type'] + 'fieldName' => $name ); + + if (isset($idElement['type'])) { + $mapping['type'] = $idElement['type']; + } if (isset($idElement['column'])) { $mapping['columnName'] = $idElement['column']; @@ -201,19 +200,21 @@ class YamlDriver extends AbstractFileDriver // Evaluate fields if (isset($element['fields'])) { foreach ($element['fields'] as $name => $fieldMapping) { - if (!isset($fieldMapping['type'])) { - throw MappingException::propertyTypeIsRequired($className, $name); - } - - $e = explode('(', $fieldMapping['type']); - $fieldMapping['type'] = $e[0]; - if (isset($e[1])) { - $fieldMapping['length'] = substr($e[1], 0, strlen($e[1]) - 1); - } + $mapping = array( - 'fieldName' => $name, - 'type' => $fieldMapping['type'] + 'fieldName' => $name ); + + if (isset($fieldMapping['type'])) { + $e = explode('(', $fieldMapping['type']); + $fieldMapping['type'] = $e[0]; + $mapping['type'] = $fieldMapping['type']; + + if (isset($e[1])) { + $fieldMapping['length'] = substr($e[1], 0, strlen($e[1]) - 1); + } + } + if (isset($fieldMapping['id'])) { $mapping['id'] = true; if (isset($fieldMapping['generator']['strategy'])) { diff --git a/lib/Doctrine/ORM/Mapping/MappingException.php b/lib/Doctrine/ORM/Mapping/MappingException.php index fcdd5c575..e32e34c16 100644 --- a/lib/Doctrine/ORM/Mapping/MappingException.php +++ b/lib/Doctrine/ORM/Mapping/MappingException.php @@ -298,4 +298,9 @@ class MappingException extends \Doctrine\ORM\ORMException { return new self("Entity '" . $className . "' has no method '" . $methodName . "' to be registered as lifecycle callback."); } -} \ No newline at end of file + + public static function invalidFetchMode($className, $annotation) + { + return new self("Entity '" . $className . "' has a mapping with invalid fetch mode '" . $annotation . "'"); + } +} diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index 82d616686..b64cacfdc 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -93,21 +93,21 @@ final class PersistentCollection implements Collection /** * Whether the collection has already been initialized. - * + * * @var boolean */ private $initialized = true; - + /** * The wrapped Collection instance. - * + * * @var Collection */ private $coll; /** * Creates a new persistent collection. - * + * * @param EntityManager $em The EntityManager the collection will be associated with. * @param ClassMetadata $class The class descriptor of the entity type of this collection. * @param array The collection elements. @@ -144,7 +144,7 @@ final class PersistentCollection implements Collection { return $this->owner; } - + public function getTypeClass() { return $this->typeClass; @@ -154,7 +154,7 @@ final class PersistentCollection implements Collection * INTERNAL: * Adds an element to a collection during hydration. This will automatically * complete bidirectional associations in the case of a one-to-many association. - * + * * @param mixed $element The element to add. */ public function hydrateAdd($element) @@ -172,7 +172,7 @@ final class PersistentCollection implements Collection $this->owner); } } - + /** * INTERNAL: * Sets a keyed element in the collection during hydration. @@ -271,7 +271,7 @@ final class PersistentCollection implements Collection { return $this->association; } - + /** * Marks this collection as changed/dirty. */ @@ -306,17 +306,17 @@ final class PersistentCollection implements Collection { $this->isDirty = $dirty; } - + /** * Sets the initialized flag of the collection, forcing it into that state. - * + * * @param boolean $bool */ public function setInitialized($bool) { $this->initialized = $bool; } - + /** * Checks whether this collection has been initialized. * @@ -377,7 +377,7 @@ final class PersistentCollection implements Collection $this->em->getUnitOfWork()->getCollectionPersister($this->association) ->deleteRows($this, $element); }*/ - + $this->initialize(); $removed = $this->coll->removeElement($element); if ($removed) { @@ -410,7 +410,7 @@ final class PersistentCollection implements Collection ->getCollectionPersister($this->association) ->contains($this, $element); } - + $this->initialize(); return $this->coll->contains($element); } @@ -468,7 +468,7 @@ final class PersistentCollection implements Collection if (!$this->initialized && $this->association['fetch'] == Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY) { return $this->em->getUnitOfWork() ->getCollectionPersister($this->association) - ->count($this) + $this->coll->count(); + ->count($this) + ($this->isDirty ? $this->coll->count() : 0); } $this->initialize(); @@ -503,7 +503,7 @@ final class PersistentCollection implements Collection $this->initialize(); return $this->coll->isEmpty(); } - + /** * {@inheritdoc} */ @@ -530,7 +530,7 @@ final class PersistentCollection implements Collection $this->initialize(); return $this->coll->filter($p); } - + /** * {@inheritdoc} */ @@ -548,7 +548,7 @@ final class PersistentCollection implements Collection $this->initialize(); return $this->coll->partition($p); } - + /** * {@inheritdoc} */ @@ -579,7 +579,7 @@ final class PersistentCollection implements Collection $this->takeSnapshot(); } } - + /** * Called by PHP when this collection is serialized. Ensures that only the * elements are properly serialized. @@ -591,7 +591,7 @@ final class PersistentCollection implements Collection { return array('coll', 'initialized'); } - + /* ArrayAccess implementation */ /** @@ -629,12 +629,12 @@ final class PersistentCollection implements Collection { return $this->remove($offset); } - + public function key() { return $this->coll->key(); } - + /** * Gets the element of the collection at the current iterator position. */ @@ -642,7 +642,7 @@ final class PersistentCollection implements Collection { return $this->coll->current(); } - + /** * Moves the internal iterator position to the next element. */ @@ -650,7 +650,7 @@ final class PersistentCollection implements Collection { return $this->coll->next(); } - + /** * Retrieves the wrapped Collection instance. */ @@ -672,7 +672,10 @@ final class PersistentCollection implements Collection */ public function slice($offset, $length = null) { - if (!$this->initialized && $this->association['fetch'] == Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY) { + if ( ! $this->initialized && + ! $this->isDirty && + $this->association['fetch'] == Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY) { + return $this->em->getUnitOfWork() ->getCollectionPersister($this->association) ->slice($this, $offset, $length); diff --git a/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php b/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php index 670cf11e7..84540a337 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php @@ -62,7 +62,7 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister { $columnName = $class->columnNames[$field]; $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) . '.' . $class->getQuotedColumnName($field, $this->_platform); - $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++); + $columnAlias = $this->getSQLColumnAlias($columnName); $this->_rsm->addFieldResult($alias, $columnAlias, $field, $class->name); return $sql . ' AS ' . $columnAlias; @@ -70,10 +70,9 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister protected function getSelectJoinColumnSQL($tableAlias, $joinColumnName, $className) { - $columnAlias = $joinColumnName . $this->_sqlAliasCounter++; - $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); - $this->_rsm->addMetaResult('r', $resultColumnName, $joinColumnName); + $columnAlias = $this->getSQLColumnAlias($joinColumnName); + $this->_rsm->addMetaResult('r', $columnAlias, $joinColumnName); return $tableAlias . '.' . $joinColumnName . ' AS ' . $columnAlias; } -} \ No newline at end of file +} diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index fab80ff29..8663f3014 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -1001,27 +1001,26 @@ class BasicEntityPersister $columnList .= $assoc2ColumnSQL; } } - - $this->_selectJoinSql .= ' LEFT JOIN'; // TODO: Inner join when all join columns are NOT nullable. $first = true; if ($assoc['isOwningSide']) { + $this->_selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($assoc['joinColumns']); $this->_selectJoinSql .= ' ' . $eagerEntity->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON '; foreach ($assoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) { if ( ! $first) { $this->_selectJoinSql .= ' AND '; } - - $this->_selectJoinSql .= $this->_getSQLTableAlias($assoc['sourceEntity']) . '.' . $sourceCol . ' = ' - . $this->_getSQLTableAlias($assoc['targetEntity'], $assocAlias) . '.' . $targetCol . ' '; + $this->_selectJoinSql .= $this->_getSQLTableAlias($assoc['sourceEntity']) . '.' . $sourceCol . ' = ' + . $this->_getSQLTableAlias($assoc['targetEntity'], $assocAlias) . '.' . $targetCol; $first = false; } } else { $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']); $owningAssoc = $eagerEntity->getAssociationMapping($assoc['mappedBy']); - $this->_selectJoinSql .= ' ' . $eagerEntity->getQuotedTableName($this->_platform) . ' ' + $this->_selectJoinSql .= ' LEFT JOIN'; + $this->_selectJoinSql .= ' ' . $eagerEntity->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) . ' ON '; foreach ($owningAssoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) { @@ -1030,7 +1029,7 @@ class BasicEntityPersister } $this->_selectJoinSql .= $this->_getSQLTableAlias($owningAssoc['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = ' - . $this->_getSQLTableAlias($owningAssoc['targetEntity']) . '.' . $targetCol . ' '; + . $this->_getSQLTableAlias($owningAssoc['targetEntity']) . '.' . $targetCol; $first = false; } } @@ -1060,9 +1059,8 @@ class BasicEntityPersister foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { if ($columnList) $columnList .= ', '; - $columnAlias = $srcColumn . $this->_sqlAliasCounter++; - $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); - $columnList .= $this->_getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias) ) + $resultColumnName = $this->getSQLColumnAlias($srcColumn); + $columnList .= $this->_getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias) ) . '.' . $srcColumn . ' AS ' . $resultColumnName; $this->_rsm->addMetaResult($alias, $resultColumnName, $srcColumn, isset($assoc['id']) && $assoc['id'] === true); } @@ -1180,10 +1178,9 @@ class BasicEntityPersister */ protected function _getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r') { - $columnName = $class->columnNames[$field]; - $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) + $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) . '.' . $class->getQuotedColumnName($field, $this->_platform); - $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++); + $columnAlias = $this->getSQLColumnAlias($class->columnNames[$field]); $this->_rsm->addFieldResult($alias, $columnAlias, $field); @@ -1500,4 +1497,37 @@ class BasicEntityPersister return (bool) $this->_conn->fetchColumn($sql, $params); } + + /** + * Generates the appropriate join SQL for the given join column. + * + * @param array $joinColumns The join columns definition of an association. + * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise. + */ + protected function getJoinSQLForJoinColumns($joinColumns) + { + // if one of the join columns is nullable, return left join + foreach($joinColumns as $joinColumn) { + if(isset($joinColumn['nullable']) && $joinColumn['nullable']){ + return 'LEFT JOIN'; + } + } + + return 'INNER JOIN'; + } + + /** + * Gets an SQL column alias for a column name. + * + * @param string $columnName + * @return string + */ + public function getSQLColumnAlias($columnName) + { + // Trim the column alias to the maximum identifier length of the platform. + // If the alias is to long, characters are cut off from the beginning. + return $this->_platform->getSQLResultCasing( + substr($columnName . $this->_sqlAliasCounter++, -$this->_platform->getMaxIdentifierLength()) + ); + } } diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index 490c3a119..cf7048549 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -165,11 +165,13 @@ class ProxyFactory { $methods = ''; + $methodNames = array(); foreach ($class->reflClass->getMethods() as $method) { /* @var $method ReflectionMethod */ - if ($method->isConstructor() || in_array(strtolower($method->getName()), array("__sleep", "__clone"))) { + if ($method->isConstructor() || in_array(strtolower($method->getName()), array("__sleep", "__clone")) || isset($methodNames[$method->getName()])) { continue; } + $methodNames[$method->getName()] = true; if ($method->isPublic() && ! $method->isFinal() && ! $method->isStatic()) { $methods .= "\n" . ' public function '; @@ -210,8 +212,12 @@ class ProxyFactory $methods .= $parameterString . ')'; $methods .= "\n" . ' {' . "\n"; if ($this->isShortIdentifierGetter($method, $class)) { + $identifier = lcfirst(substr($method->getName(), 3)); + + $cast = in_array($class->fieldMappings[$identifier]['type'], array('integer', 'smallint')) ? '(int) ' : ''; + $methods .= ' if ($this->__isInitialized__ === false) {' . "\n"; - $methods .= ' return $this->_identifier["' . lcfirst(substr($method->getName(), 3)) . '"];' . "\n"; + $methods .= ' return ' . $cast . '$this->_identifier["' . $identifier . '"];' . "\n"; $methods .= ' }' . "\n"; } $methods .= ' $this->__load();' . "\n"; @@ -230,13 +236,14 @@ class ProxyFactory */ private function isShortIdentifierGetter($method, $class) { - $identifier = lcfirst(substr($method->getName(), 3)); + $identifier = lcfirst(substr($method->getName(), 3)); return ( $method->getNumberOfParameters() == 0 && substr($method->getName(), 0, 3) == "get" && in_array($identifier, $class->identifier, true) && $class->hasField($identifier) && (($method->getEndLine() - $method->getStartLine()) <= 4) + && in_array($class->fieldMappings[$identifier]['type'], array('integer', 'bigint', 'smallint', 'string')) ); } diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index 293c64391..ac1eb75d7 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -526,7 +526,7 @@ final class Query extends AbstractQuery * * @param array $params The query parameters. * @param integer $hydrationMode The hydration mode to use. - * @return IterableResult + * @return \Doctrine\ORM\Internal\Hydration\IterableResult */ public function iterate(array $params = array(), $hydrationMode = self::HYDRATE_OBJECT) { diff --git a/lib/Doctrine/ORM/Query/Expr/Composite.php b/lib/Doctrine/ORM/Query/Expr/Composite.php index 0d606a9b0..036b241a5 100644 --- a/lib/Doctrine/ORM/Query/Expr/Composite.php +++ b/lib/Doctrine/ORM/Query/Expr/Composite.php @@ -39,30 +39,30 @@ class Composite extends Base if ($this->count() === 1) { return (string) $this->_parts[0]; } - + $components = array(); - + foreach ($this->_parts as $part) { $components[] = $this->processQueryPart($part); } - + return implode($this->_separator, $components); } - - + + private function processQueryPart($part) { $queryPart = (string) $part; - + if (is_object($part) && $part instanceof self && $part->count() > 1) { return $this->_preSeparator . $queryPart . $this->_postSeparator; } - + // Fixes DDC-1237: User may have added a where item containing nested expression (with "OR" or "AND") - if (mb_stripos($queryPart, ' OR ') !== false || mb_stripos($queryPart, ' AND ') !== false) { + if (stripos($queryPart, ' OR ') !== false || stripos($queryPart, ' AND ') !== false) { return $this->_preSeparator . $queryPart . $this->_postSeparator; } - + return $queryPart; } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 9fc30bb8c..2af4bb452 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -141,9 +141,9 @@ class Parser */ public function __construct(Query $query) { - $this->_query = $query; - $this->_em = $query->getEntityManager(); - $this->_lexer = new Lexer($query->getDql()); + $this->_query = $query; + $this->_em = $query->getEntityManager(); + $this->_lexer = new Lexer($query->getDql()); $this->_parserResult = new ParserResult(); } @@ -226,6 +226,11 @@ class Parser $this->_processDeferredResultVariables(); } + $this->_processRootEntityAliasSelected(); + + // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot! + $this->fixIdentificationVariableOrder($AST); + return $AST; } @@ -241,11 +246,10 @@ class Parser */ public function match($token) { + $lookaheadType = $this->_lexer->lookahead['type']; + // short-circuit on first condition, usually types match - if ($this->_lexer->lookahead['type'] !== $token && - $token !== Lexer::T_IDENTIFIER && - $this->_lexer->lookahead['type'] <= Lexer::T_IDENTIFIER - ) { + if ($lookaheadType !== $token && $token !== Lexer::T_IDENTIFIER && $lookaheadType <= Lexer::T_IDENTIFIER) { $this->syntaxError($this->_lexer->getLiteral($token)); } @@ -281,9 +285,6 @@ class Parser { $AST = $this->getAST(); - $this->fixIdentificationVariableOrder($AST); - $this->assertSelectEntityRootAliasRequirement(); - if (($customWalkers = $this->_query->getHint(Query::HINT_CUSTOM_TREE_WALKERS)) !== false) { $this->_customTreeWalkers = $customWalkers; } @@ -300,68 +301,57 @@ class Parser $treeWalkerChain->addTreeWalker($walker); } - if ($AST instanceof AST\SelectStatement) { - $treeWalkerChain->walkSelectStatement($AST); - } else if ($AST instanceof AST\UpdateStatement) { - $treeWalkerChain->walkUpdateStatement($AST); - } else { - $treeWalkerChain->walkDeleteStatement($AST); + switch (true) { + case ($AST instanceof AST\UpdateStatement): + $treeWalkerChain->walkUpdateStatement($AST); + break; + + case ($AST instanceof AST\DeleteStatement): + $treeWalkerChain->walkDeleteStatement($AST); + break; + + case ($AST instanceof AST\SelectStatement): + default: + $treeWalkerChain->walkSelectStatement($AST); } } - if ($this->_customOutputWalker) { - $outputWalker = new $this->_customOutputWalker( - $this->_query, $this->_parserResult, $this->_queryComponents - ); - } else { - $outputWalker = new SqlWalker( - $this->_query, $this->_parserResult, $this->_queryComponents - ); - } + $outputWalkerClass = $this->_customOutputWalker ?: __NAMESPACE__ . '\SqlWalker'; + $outputWalker = new $outputWalkerClass($this->_query, $this->_parserResult, $this->_queryComponents); // Assign an SQL executor to the parser result $this->_parserResult->setSqlExecutor($outputWalker->getExecutor($AST)); return $this->_parserResult; } - - private function assertSelectEntityRootAliasRequirement() - { - if ( count($this->_identVariableExpressions) > 0) { - $foundRootEntity = false; - foreach ($this->_identVariableExpressions AS $dqlAlias => $expr) { - if (isset($this->_queryComponents[$dqlAlias]) && $this->_queryComponents[$dqlAlias]['parent'] === null) { - $foundRootEntity = true; - } - } - - if (!$foundRootEntity) { - $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.'); - } - } - } - + /** * Fix order of identification variables. - * + * * They have to appear in the select clause in the same order as the * declarations (from ... x join ... y join ... z ...) appear in the query * as the hydration process relies on that order for proper operation. - * + * * @param AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST * @return void */ private function fixIdentificationVariableOrder($AST) { - if ( count($this->_identVariableExpressions) > 1) { - foreach ($this->_queryComponents as $dqlAlias => $qComp) { - if (isset($this->_identVariableExpressions[$dqlAlias])) { - $expr = $this->_identVariableExpressions[$dqlAlias]; - $key = array_search($expr, $AST->selectClause->selectExpressions); - unset($AST->selectClause->selectExpressions[$key]); - $AST->selectClause->selectExpressions[] = $expr; - } + if (count($this->_identVariableExpressions) <= 1) { + return; + } + + foreach ($this->_queryComponents as $dqlAlias => $qComp) { + if ( ! isset($this->_identVariableExpressions[$dqlAlias])) { + continue; } + + $expr = $this->_identVariableExpressions[$dqlAlias]; + $key = array_search($expr, $AST->selectClause->selectExpressions); + + unset($AST->selectClause->selectExpressions[$key]); + + $AST->selectClause->selectExpressions[] = $expr; } } @@ -380,19 +370,10 @@ class Parser } $tokenPos = (isset($token['position'])) ? $token['position'] : '-1'; + $message = "line 0, col {$tokenPos}: Error: "; - - if ($expected !== '') { - $message .= "Expected {$expected}, got "; - } else { - $message .= 'Unexpected '; - } - - if ($this->_lexer->lookahead === null) { - $message .= 'end of string.'; - } else { - $message .= "'{$token['value']}'"; - } + $message .= ($expected !== '') ? "Expected {$expected}, got " : 'Unexpected '; + $message .= ($this->_lexer->lookahead === null) ? 'end of string.' : "'{$token['value']}'"; throw QueryException::syntaxError($message); } @@ -415,18 +396,19 @@ class Parser $distance = 12; // Find a position of a final word to display in error string - $dql = $this->_query->getDql(); + $dql = $this->_query->getDql(); $length = strlen($dql); - $pos = $token['position'] + $distance; - $pos = strpos($dql, ' ', ($length > $pos) ? $pos : $length); + $pos = $token['position'] + $distance; + $pos = strpos($dql, ' ', ($length > $pos) ? $pos : $length); $length = ($pos !== false) ? $pos - $token['position'] : $distance; - // Building informative message - $message = 'line 0, col ' . ( - (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1' - ) . " near '" . substr($dql, $token['position'], $length) . "': Error: " . $message; + $tokenPos = (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1'; + $tokenStr = substr($dql, $token['position'], $length); - throw \Doctrine\ORM\Query\QueryException::semanticalError($message); + // Building informative message + $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message; + + throw QueryException::semanticalError($message); } /** @@ -460,15 +442,22 @@ class Parser $numUnmatched = 1; while ($numUnmatched > 0 && $token !== null) { - if ($token['value'] == ')') { - --$numUnmatched; - } else if ($token['value'] == '(') { - ++$numUnmatched; + switch ($token['type']) { + case Lexer::T_OPEN_PARENTHESIS: + ++$numUnmatched; + break; + + case Lexer::T_CLOSE_PARENTHESIS: + --$numUnmatched; + break; + + default: + // Do nothing } $token = $this->_lexer->peek(); } - + $this->_lexer->resetPeek(); return $token; @@ -481,7 +470,7 @@ class Parser */ private function _isMathOperator($token) { - return in_array($token['value'], array("+", "-", "/", "*")); + return in_array($token['type'], array(Lexer::T_PLUS, Lexer::T_MINUS, Lexer::T_DIVIDE, Lexer::T_MULTIPLY)); } /** @@ -491,12 +480,13 @@ class Parser */ private function _isFunction() { - $peek = $this->_lexer->peek(); + $peek = $this->_lexer->peek(); $nextpeek = $this->_lexer->peek(); + $this->_lexer->resetPeek(); // We deny the COUNT(SELECT * FROM User u) here. COUNT won't be considered a function - return ($peek['value'] === '(' && $nextpeek['type'] !== Lexer::T_SELECT); + return ($peek['type'] === Lexer::T_OPEN_PARENTHESIS && $nextpeek['type'] !== Lexer::T_SELECT); } /** @@ -506,35 +496,17 @@ class Parser */ private function _isAggregateFunction($tokenType) { - return $tokenType == Lexer::T_AVG || $tokenType == Lexer::T_MIN || - $tokenType == Lexer::T_MAX || $tokenType == Lexer::T_SUM || - $tokenType == Lexer::T_COUNT; + return in_array($tokenType, array(Lexer::T_AVG, Lexer::T_MIN, Lexer::T_MAX, Lexer::T_SUM, Lexer::T_COUNT)); } /** - * Checks whether the current lookahead token of the lexer has the type - * T_ALL, T_ANY or T_SOME. + * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME. * * @return boolean */ private function _isNextAllAnySome() { - return $this->_lexer->lookahead['type'] === Lexer::T_ALL || - $this->_lexer->lookahead['type'] === Lexer::T_ANY || - $this->_lexer->lookahead['type'] === Lexer::T_SOME; - } - - /** - * Checks whether the next 2 tokens start a subselect. - * - * @return boolean TRUE if the next 2 tokens start a subselect, FALSE otherwise. - */ - private function _isSubselect() - { - $la = $this->_lexer->lookahead; - $next = $this->_lexer->glimpse(); - - return ($la['value'] === '(' && $next['type'] === Lexer::T_SELECT); + return in_array($this->_lexer->lookahead['type'], array(Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME)); } /** @@ -586,12 +558,13 @@ class Parser $class = $this->_queryComponents[$expr->identificationVariable]['metadata']; foreach ($expr->partialFieldSet as $field) { - if ( ! isset($class->fieldMappings[$field])) { - $this->semanticalError( - "There is no mapped field named '$field' on class " . $class->name . ".", - $deferredItem['token'] - ); + if (isset($class->fieldMappings[$field])) { + continue; } + + $this->semanticalError( + "There is no mapped field named '$field' on class " . $class->name . ".", $deferredItem['token'] + ); } if (array_intersect($class->identifier, $expr->partialFieldSet) != $class->identifier) { @@ -662,7 +635,7 @@ class Parser if (($field = $pathExpression->field) === null) { $field = $pathExpression->field = $class->identifier[0]; } - + // Check if field or association exists if ( ! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) { $this->semanticalError( @@ -671,17 +644,14 @@ class Parser ); } - if (isset($class->fieldMappings[$field])) { - $fieldType = AST\PathExpression::TYPE_STATE_FIELD; - } else { - $assoc = $class->associationMappings[$field]; - $class = $this->_em->getClassMetadata($assoc['targetEntity']); + $fieldType = AST\PathExpression::TYPE_STATE_FIELD; - if ($assoc['type'] & ClassMetadata::TO_ONE) { - $fieldType = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; - } else { - $fieldType = AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION; - } + if (isset($class->associationMappings[$field])) { + $assoc = $class->associationMappings[$field]; + + $fieldType = ($assoc['type'] & ClassMetadata::TO_ONE) + ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION + : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION; } // Validate if PathExpression is one of the expected types @@ -717,12 +687,31 @@ class Parser $this->semanticalError($semanticalError, $deferredItem['token']); } - + // We need to force the type in PathExpression $pathExpression->type = $fieldType; } } + private function _processRootEntityAliasSelected() + { + if ( ! count($this->_identVariableExpressions)) { + return; + } + + $foundRootEntity = false; + + foreach ($this->_identVariableExpressions AS $dqlAlias => $expr) { + if (isset($this->_queryComponents[$dqlAlias]) && $this->_queryComponents[$dqlAlias]['parent'] === null) { + $foundRootEntity = true; + } + } + + if ( ! $foundRootEntity) { + $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.'); + } + } + /** * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement * @@ -738,12 +727,15 @@ class Parser case Lexer::T_SELECT: $statement = $this->SelectStatement(); break; + case Lexer::T_UPDATE: $statement = $this->UpdateStatement(); break; + case Lexer::T_DELETE: $statement = $this->DeleteStatement(); break; + default: $this->syntaxError('SELECT, UPDATE or DELETE'); break; @@ -766,17 +758,10 @@ class Parser { $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause()); - $selectStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) - ? $this->WhereClause() : null; - - $selectStatement->groupByClause = $this->_lexer->isNextToken(Lexer::T_GROUP) - ? $this->GroupByClause() : null; - - $selectStatement->havingClause = $this->_lexer->isNextToken(Lexer::T_HAVING) - ? $this->HavingClause() : null; - - $selectStatement->orderByClause = $this->_lexer->isNextToken(Lexer::T_ORDER) - ? $this->OrderByClause() : null; + $selectStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null; + $selectStatement->groupByClause = $this->_lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null; + $selectStatement->havingClause = $this->_lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null; + $selectStatement->orderByClause = $this->_lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null; return $selectStatement; } @@ -789,8 +774,8 @@ class Parser public function UpdateStatement() { $updateStatement = new AST\UpdateStatement($this->UpdateClause()); - $updateStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) - ? $this->WhereClause() : null; + + $updateStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null; return $updateStatement; } @@ -803,8 +788,8 @@ class Parser public function DeleteStatement() { $deleteStatement = new AST\DeleteStatement($this->DeleteClause()); - $deleteStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) - ? $this->WhereClause() : null; + + $deleteStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null; return $deleteStatement; } @@ -842,9 +827,7 @@ class Parser $exists = isset($this->_queryComponents[$aliasIdentVariable]); if ($exists) { - $this->semanticalError( - "'$aliasIdentVariable' is already defined.", $this->_lexer->token - ); + $this->semanticalError("'$aliasIdentVariable' is already defined.", $this->_lexer->token); } return $aliasIdentVariable; @@ -863,6 +846,7 @@ class Parser if (strrpos($schemaName, ':') !== false) { list($namespaceAlias, $simpleClassName) = explode(':', $schemaName); + $schemaName = $this->_em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName; } @@ -888,9 +872,7 @@ class Parser $exists = isset($this->_queryComponents[$resultVariable]); if ($exists) { - $this->semanticalError( - "'$resultVariable' is already defined.", $this->_lexer->token - ); + $this->semanticalError("'$resultVariable' is already defined.", $this->_lexer->token); } return $resultVariable; @@ -924,11 +906,13 @@ class Parser */ public function JoinAssociationPathExpression() { - $token = $this->_lexer->lookahead; + $token = $this->_lexer->lookahead; $identVariable = $this->IdentificationVariable(); - if (!isset($this->_queryComponents[$identVariable])) { - $this->semanticalError('Identification Variable ' . $identVariable .' used in join path expression but was not defined before.'); + if ( ! isset($this->_queryComponents[$identVariable])) { + $this->semanticalError( + 'Identification Variable ' . $identVariable .' used in join path expression but was not defined before.' + ); } $this->match(Lexer::T_DOT); @@ -968,7 +952,7 @@ class Parser $field = $this->_lexer->token['value']; } - + // Creating AST node $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field); @@ -1051,6 +1035,7 @@ class Parser // Check for DISTINCT if ($this->_lexer->isNextToken(Lexer::T_DISTINCT)) { $this->match(Lexer::T_DISTINCT); + $isDistinct = true; } @@ -1060,6 +1045,7 @@ class Parser while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); + $selectExpressions[] = $this->SelectExpression(); } @@ -1078,6 +1064,7 @@ class Parser if ($this->_lexer->isNextToken(Lexer::T_DISTINCT)) { $this->match(Lexer::T_DISTINCT); + $isDistinct = true; } @@ -1112,6 +1099,7 @@ class Parser 'nestingLevel' => $this->_nestingLevel, 'token' => $token, ); + $this->_queryComponents[$aliasIdentificationVariable] = $queryComponent; $this->match(Lexer::T_SET); @@ -1121,6 +1109,7 @@ class Parser while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); + $updateItems[] = $this->UpdateItem(); } @@ -1164,6 +1153,7 @@ class Parser 'nestingLevel' => $this->_nestingLevel, 'token' => $token, ); + $this->_queryComponents[$aliasIdentificationVariable] = $queryComponent; return $deleteClause; @@ -1177,11 +1167,13 @@ class Parser public function FromClause() { $this->match(Lexer::T_FROM); + $identificationVariableDeclarations = array(); $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); + $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); } @@ -1196,11 +1188,13 @@ class Parser public function SubselectFromClause() { $this->match(Lexer::T_FROM); + $identificationVariables = array(); $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); + $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); } @@ -1245,6 +1239,7 @@ class Parser while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); + $groupByItems[] = $this->GroupByItem(); } @@ -1266,6 +1261,7 @@ class Parser while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); + $orderByItems[] = $this->OrderByItem(); } @@ -1284,17 +1280,10 @@ class Parser $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause()); - $subselect->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) - ? $this->WhereClause() : null; - - $subselect->groupByClause = $this->_lexer->isNextToken(Lexer::T_GROUP) - ? $this->GroupByClause() : null; - - $subselect->havingClause = $this->_lexer->isNextToken(Lexer::T_HAVING) - ? $this->HavingClause() : null; - - $subselect->orderByClause = $this->_lexer->isNextToken(Lexer::T_ORDER) - ? $this->OrderByClause() : null; + $subselect->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null; + $subselect->groupByClause = $this->_lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null; + $subselect->havingClause = $this->_lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null; + $subselect->orderByClause = $this->_lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null; // Decrease query nesting level $this->_nestingLevel--; @@ -1331,11 +1320,11 @@ class Parser if ($glimpse['type'] == Lexer::T_DOT) { return $this->SingleValuedPathExpression(); } - + $token = $this->_lexer->lookahead; $identVariable = $this->IdentificationVariable(); - if (!isset($this->_queryComponents[$identVariable])) { + if ( ! isset($this->_queryComponents[$identVariable])) { $this->semanticalError('Cannot group by undefined identification variable.'); } @@ -1353,21 +1342,26 @@ class Parser // We need to check if we are in a ResultVariable or StateFieldPathExpression $glimpse = $this->_lexer->glimpse(); - - $expr = ($glimpse['type'] != Lexer::T_DOT) - ? $this->ResultVariable() - : $this->SingleValuedPathExpression(); + $expr = ($glimpse['type'] != Lexer::T_DOT) ? $this->ResultVariable() : $this->SingleValuedPathExpression(); $item = new AST\OrderByItem($expr); - if ($this->_lexer->isNextToken(Lexer::T_ASC)) { - $this->match(Lexer::T_ASC); - } else if ($this->_lexer->isNextToken(Lexer::T_DESC)) { - $this->match(Lexer::T_DESC); - $type = 'DESC'; + switch (true) { + case ($this->_lexer->isNextToken(Lexer::T_DESC)): + $this->match(Lexer::T_DESC); + $type = 'DESC'; + break; + + case ($this->_lexer->isNextToken(Lexer::T_ASC)): + $this->match(Lexer::T_ASC); + break; + + default: + // Do nothing } $item->type = $type; + return $item; } @@ -1386,9 +1380,13 @@ class Parser { if ($this->_lexer->isNextToken(Lexer::T_NULL)) { $this->match(Lexer::T_NULL); + return null; - } else if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { + } + + if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { $this->match(Lexer::T_INPUT_PARAMETER); + return new AST\InputParameter($this->_lexer->token['value']); } @@ -1451,9 +1449,8 @@ class Parser */ public function JoinVariableDeclaration() { - $join = $this->Join(); - $indexBy = $this->_lexer->isNextToken(Lexer::T_INDEX) - ? $this->IndexBy() : null; + $join = $this->Join(); + $indexBy = $this->_lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null; return new AST\JoinVariableDeclaration($join, $indexBy); } @@ -1484,6 +1481,7 @@ class Parser 'nestingLevel' => $this->_nestingLevel, 'token' => $token ); + $this->_queryComponents[$aliasIdentificationVariable] = $queryComponent; return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable); @@ -1502,18 +1500,20 @@ class Parser $partialFieldSet = array(); $identificationVariable = $this->IdentificationVariable(); - $this->match(Lexer::T_DOT); + $this->match(Lexer::T_DOT); $this->match(Lexer::T_OPEN_CURLY_BRACE); $this->match(Lexer::T_IDENTIFIER); + $partialFieldSet[] = $this->_lexer->token['value']; while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); $this->match(Lexer::T_IDENTIFIER); + $partialFieldSet[] = $this->_lexer->token['value']; } - + $this->match(Lexer::T_CLOSE_CURLY_BRACE); $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet); @@ -1539,18 +1539,26 @@ class Parser // Check Join type $joinType = AST\Join::JOIN_TYPE_INNER; - if ($this->_lexer->isNextToken(Lexer::T_LEFT)) { - $this->match(Lexer::T_LEFT); + switch (true) { + case ($this->_lexer->isNextToken(Lexer::T_LEFT)): + $this->match(Lexer::T_LEFT); - // Possible LEFT OUTER join - if ($this->_lexer->isNextToken(Lexer::T_OUTER)) { - $this->match(Lexer::T_OUTER); - $joinType = AST\Join::JOIN_TYPE_LEFTOUTER; - } else { $joinType = AST\Join::JOIN_TYPE_LEFT; - } - } else if ($this->_lexer->isNextToken(Lexer::T_INNER)) { - $this->match(Lexer::T_INNER); + + // Possible LEFT OUTER join + if ($this->_lexer->isNextToken(Lexer::T_OUTER)) { + $this->match(Lexer::T_OUTER); + + $joinType = AST\Join::JOIN_TYPE_LEFTOUTER; + } + break; + + case ($this->_lexer->isNextToken(Lexer::T_INNER)): + $this->match(Lexer::T_INNER); + break; + + default: + // Do nothing } $this->match(Lexer::T_JOIN); @@ -1566,7 +1574,7 @@ class Parser // Verify that the association exists. $parentClass = $this->_queryComponents[$joinPathExpression->identificationVariable]['metadata']; - $assocField = $joinPathExpression->associationField; + $assocField = $joinPathExpression->associationField; if ( ! $parentClass->hasAssociation($assocField)) { $this->semanticalError( @@ -1585,6 +1593,7 @@ class Parser 'nestingLevel' => $this->_nestingLevel, 'token' => $token ); + $this->_queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; // Create AST node @@ -1593,6 +1602,7 @@ class Parser // Check for ad-hoc Join conditions if ($this->_lexer->isNextToken(Lexer::T_WITH)) { $this->match(Lexer::T_WITH); + $join->conditionalExpression = $this->ConditionalExpression(); } @@ -1626,180 +1636,198 @@ class Parser public function ScalarExpression() { $lookahead = $this->_lexer->lookahead['type']; - if ($lookahead === Lexer::T_IDENTIFIER) { - $this->_lexer->peek(); // lookahead => '.' - $this->_lexer->peek(); // lookahead => token after '.' - $peek = $this->_lexer->peek(); // lookahead => token after the token after the '.' - $this->_lexer->resetPeek(); - if ($this->_isMathOperator($peek)) { + switch ($lookahead) { + case Lexer::T_IDENTIFIER: + $this->_lexer->peek(); // lookahead => '.' + $this->_lexer->peek(); // lookahead => token after '.' + $peek = $this->_lexer->peek(); // lookahead => token after the token after the '.' + $this->_lexer->resetPeek(); + + if ($this->_isMathOperator($peek)) { + return $this->SimpleArithmeticExpression(); + } + + return $this->StateFieldPathExpression(); + + case Lexer::T_INTEGER: + case Lexer::T_FLOAT: return $this->SimpleArithmeticExpression(); - } - return $this->StateFieldPathExpression(); - } else if ($lookahead == Lexer::T_INTEGER || $lookahead == Lexer::T_FLOAT) { - return $this->SimpleArithmeticExpression(); - } else if ($lookahead == Lexer::T_CASE || $lookahead == Lexer::T_COALESCE || $lookahead == Lexer::T_NULLIF) { - // Since NULLIF and COALESCE can be identified as a function, - // we need to check if before check for FunctionDeclaration - return $this->CaseExpression(); - } else if ($this->_isFunction() || $this->_isAggregateFunction($this->_lexer->lookahead['type'])) { - // We may be in an ArithmeticExpression (find the matching ")" and inspect for Math operator) - $this->_lexer->peek(); // "(" - $peek = $this->_peekBeyondClosingParenthesis(); + case Lexer::T_STRING: + return $this->StringPrimary(); - if ($this->_isMathOperator($peek)) { - return $this->SimpleArithmeticExpression(); - } + case Lexer::T_TRUE: + case Lexer::T_FALSE: + $this->match($lookahead); - if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { - return $this->AggregateExpression(); - } - - return $this->FunctionDeclaration(); - } else if ($lookahead == Lexer::T_STRING) { - return $this->StringPrimary(); - } else if ($lookahead == Lexer::T_INPUT_PARAMETER) { - return $this->InputParameter(); - } else if ($lookahead == Lexer::T_TRUE || $lookahead == Lexer::T_FALSE) { - $this->match($lookahead); - return new AST\Literal(AST\Literal::BOOLEAN, $this->_lexer->token['value']); - } else { - $this->syntaxError(); + return new AST\Literal(AST\Literal::BOOLEAN, $this->_lexer->token['value']); + + case Lexer::T_INPUT_PARAMETER: + return $this->InputParameter(); + + case Lexer::T_CASE: + case Lexer::T_COALESCE: + case Lexer::T_NULLIF: + // Since NULLIF and COALESCE can be identified as a function, + // we need to check if before check for FunctionDeclaration + return $this->CaseExpression(); + + default: + if ( ! ($this->_isFunction() || $this->_isAggregateFunction($lookahead))) { + $this->syntaxError(); + } + + // We may be in an ArithmeticExpression (find the matching ")" and inspect for Math operator) + $this->_lexer->peek(); // "(" + $peek = $this->_peekBeyondClosingParenthesis(); + + if ($this->_isMathOperator($peek)) { + return $this->SimpleArithmeticExpression(); + } + + if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { + return $this->AggregateExpression(); + } + + return $this->FunctionDeclaration(); } } /** - * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression - * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" - * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression - * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" - * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator - * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression - * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" + * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression + * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" + * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression + * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" + * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator + * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression + * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" - * + * * @return mixed One of the possible expressions or subexpressions. */ public function CaseExpression() { $lookahead = $this->_lexer->lookahead['type']; - + switch ($lookahead) { case Lexer::T_NULLIF: return $this->NullIfExpression(); - + case Lexer::T_COALESCE: return $this->CoalesceExpression(); - + case Lexer::T_CASE: $this->_lexer->resetPeek(); $peek = $this->_lexer->peek(); - - return ($peek['type'] === Lexer::T_WHEN) - ? $this->GeneralCaseExpression() - : $this->SimpleCaseExpression(); - + + if ($peek['type'] === Lexer::T_WHEN) { + return $this->GeneralCaseExpression(); + } + + return $this->SimpleCaseExpression(); + default: // Do nothing break; } - + $this->syntaxError(); } - + /** * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" - * - * @return Doctrine\ORM\Query\AST\CoalesceExpression + * + * @return Doctrine\ORM\Query\AST\CoalesceExpression */ public function CoalesceExpression() { $this->match(Lexer::T_COALESCE); $this->match(Lexer::T_OPEN_PARENTHESIS); - + // Process ScalarExpressions (1..N) $scalarExpressions = array(); $scalarExpressions[] = $this->ScalarExpression(); while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); + $scalarExpressions[] = $this->ScalarExpression(); } - + $this->match(Lexer::T_CLOSE_PARENTHESIS); - + return new AST\CoalesceExpression($scalarExpressions); } - + /** * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" - * - * @return Doctrine\ORM\Query\AST\NullIfExpression + * + * @return Doctrine\ORM\Query\AST\NullIfExpression */ public function NullIfExpression() { $this->match(Lexer::T_NULLIF); $this->match(Lexer::T_OPEN_PARENTHESIS); - + $firstExpression = $this->ScalarExpression(); $this->match(Lexer::T_COMMA); $secondExpression = $this->ScalarExpression(); - + $this->match(Lexer::T_CLOSE_PARENTHESIS); return new AST\NullIfExpression($firstExpression, $secondExpression); } - + /** - * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" - * - * @return Doctrine\ORM\Query\AST\GeneralExpression + * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" + * + * @return Doctrine\ORM\Query\AST\GeneralExpression */ public function GeneralCaseExpression() { $this->match(Lexer::T_CASE); - + // Process WhenClause (1..N) $whenClauses = array(); - + do { $whenClauses[] = $this->WhenClause(); } while ($this->_lexer->isNextToken(Lexer::T_WHEN)); - + $this->match(Lexer::T_ELSE); $scalarExpression = $this->ScalarExpression(); $this->match(Lexer::T_END); - + return new AST\GeneralCaseExpression($whenClauses, $scalarExpression); } - + /** - * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" - * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator + * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" + * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator */ public function SimpleCaseExpression() { $this->match(Lexer::T_CASE); $caseOperand = $this->StateFieldPathExpression(); - + // Process SimpleWhenClause (1..N) $simpleWhenClauses = array(); - + do { $simpleWhenClauses[] = $this->SimpleWhenClause(); } while ($this->_lexer->isNextToken(Lexer::T_WHEN)); - + $this->match(Lexer::T_ELSE); $scalarExpression = $this->ScalarExpression(); $this->match(Lexer::T_END); - + return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression); } - + /** - * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression - * + * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression + * * @return Doctrine\ORM\Query\AST\WhenExpression */ public function WhenClause() @@ -1807,13 +1835,13 @@ class Parser $this->match(Lexer::T_WHEN); $conditionalExpression = $this->ConditionalExpression(); $this->match(Lexer::T_THEN); - + return new AST\WhenClause($conditionalExpression, $this->ScalarExpression()); } - + /** - * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression - * + * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression + * * @return Doctrine\ORM\Query\AST\SimpleWhenExpression */ public function SimpleWhenClause() @@ -1821,109 +1849,105 @@ class Parser $this->match(Lexer::T_WHEN); $conditionalExpression = $this->ScalarExpression(); $this->match(Lexer::T_THEN); - + return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression()); } /** - * SelectExpression ::= - * IdentificationVariable | StateFieldPathExpression | - * (AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] ["HIDDEN"] AliasResultVariable] + * SelectExpression ::= ( + * IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | + * PartialObjectExpression | "(" Subselect ")" | CaseExpression + * ) [["AS"] ["HIDDEN"] AliasResultVariable] * * @return Doctrine\ORM\Query\AST\SelectExpression */ public function SelectExpression() { - $expression = null; + $expression = null; $identVariable = null; - $hiddenAliasResultVariable = false; - $fieldAliasIdentificationVariable = null; - $peek = $this->_lexer->glimpse(); + $peek = $this->_lexer->glimpse(); - $supportsAlias = true; - - if ($peek['value'] != '(' && $this->_lexer->lookahead['type'] === Lexer::T_IDENTIFIER) { - if ($peek['value'] == '.') { - // ScalarExpression - $expression = $this->ScalarExpression(); - } else { - $supportsAlias = false; - $expression = $identVariable = $this->IdentificationVariable(); - } - } else if ($this->_lexer->lookahead['value'] == '(') { - if ($peek['type'] == Lexer::T_SELECT) { - // Subselect - $this->match(Lexer::T_OPEN_PARENTHESIS); - $expression = $this->Subselect(); - $this->match(Lexer::T_CLOSE_PARENTHESIS); - } else { - // Shortcut: ScalarExpression => SimpleArithmeticExpression - $expression = $this->SimpleArithmeticExpression(); - } + if ($this->_lexer->lookahead['type'] === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT) { + // ScalarExpression (u.name) + $expression = $this->ScalarExpression(); + } else if ($this->_lexer->lookahead['type'] === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS) { + // IdentificationVariable (u) + $expression = $identVariable = $this->IdentificationVariable(); + } else if (in_array($this->_lexer->lookahead['type'], array(Lexer::T_CASE, Lexer::T_COALESCE, Lexer::T_NULLIF))) { + // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...)) + $expression = $this->CaseExpression(); } else if ($this->_isFunction()) { + // DQL Function (SUM(u.value) or SUM(u.value) + 1) $this->_lexer->peek(); // "(" - + $lookaheadType = $this->_lexer->lookahead['type']; $beyond = $this->_peekBeyondClosingParenthesis(); - + if ($this->_isMathOperator($beyond)) { + // SUM(u.id) + COUNT(u.id) $expression = $this->ScalarExpression(); } else if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { + // COUNT(u.id) $expression = $this->AggregateExpression(); - } else if (in_array($lookaheadType, array(Lexer::T_COALESCE, Lexer::T_NULLIF))) { - $expression = $this->CaseExpression(); } else { - // Shortcut: ScalarExpression => Function + // SUM(u.id) $expression = $this->FunctionDeclaration(); } - } else if ($this->_lexer->lookahead['type'] == Lexer::T_PARTIAL) { - $supportsAlias = false; + } else if ($this->_lexer->lookahead['type'] === Lexer::T_PARTIAL) { + // PartialObjectExpression (PARTIAL u.{id, name}) $expression = $this->PartialObjectExpression(); $identVariable = $expression->identificationVariable; - } else if ($this->_lexer->lookahead['type'] == Lexer::T_INTEGER || - $this->_lexer->lookahead['type'] == Lexer::T_FLOAT || - $this->_lexer->lookahead['type'] == Lexer::T_STRING) { + } else if ($this->_lexer->lookahead['type'] === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT) { + // Subselect + $this->match(Lexer::T_OPEN_PARENTHESIS); + $expression = $this->Subselect(); + $this->match(Lexer::T_CLOSE_PARENTHESIS); + } else if (in_array($this->_lexer->lookahead['type'], array(Lexer::T_OPEN_PARENTHESIS, Lexer::T_INTEGER, Lexer::T_FLOAT, Lexer::T_STRING))) { // Shortcut: ScalarExpression => SimpleArithmeticExpression $expression = $this->SimpleArithmeticExpression(); - } else if ($this->_lexer->lookahead['type'] == Lexer::T_CASE) { - $expression = $this->CaseExpression(); } else { $this->syntaxError( - 'IdentificationVariable | StateFieldPathExpression | AggregateExpression | "(" Subselect ")" | ScalarExpression', + 'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression', $this->_lexer->lookahead ); } - if ($supportsAlias) { - if ($this->_lexer->isNextToken(Lexer::T_AS)) { - $this->match(Lexer::T_AS); - } - - if ($this->_lexer->isNextToken(Lexer::T_HIDDEN)) { - $this->match(Lexer::T_HIDDEN); - - $hiddenAliasResultVariable = true; - } + // [["AS"] ["HIDDEN"] AliasResultVariable] - if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) { - $token = $this->_lexer->lookahead; - $fieldAliasIdentificationVariable = $this->AliasResultVariable(); - - // Include AliasResultVariable in query components. - $this->_queryComponents[$fieldAliasIdentificationVariable] = array( - 'resultVariable' => $expression, - 'nestingLevel' => $this->_nestingLevel, - 'token' => $token, - ); - } + if ($this->_lexer->isNextToken(Lexer::T_AS)) { + $this->match(Lexer::T_AS); } - $expr = new AST\SelectExpression($expression, $fieldAliasIdentificationVariable, $hiddenAliasResultVariable); - - if ( ! $supportsAlias) { + $hiddenAliasResultVariable = false; + + if ($this->_lexer->isNextToken(Lexer::T_HIDDEN)) { + $this->match(Lexer::T_HIDDEN); + + $hiddenAliasResultVariable = true; + } + + $aliasResultVariable = null; + + if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) { + $token = $this->_lexer->lookahead; + $aliasResultVariable = $this->AliasResultVariable(); + + // Include AliasResultVariable in query components. + $this->_queryComponents[$aliasResultVariable] = array( + 'resultVariable' => $expression, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $token, + ); + } + + // AST + + $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable); + + if ($identVariable) { $this->_identVariableExpressions[$identVariable] = $expr; } - + return $expr; } @@ -1940,11 +1964,9 @@ class Parser if ($peek['value'] != '(' && $this->_lexer->lookahead['type'] === Lexer::T_IDENTIFIER) { // SingleValuedPathExpression | IdentificationVariable - if ($peek['value'] == '.') { - $expression = $this->StateFieldPathExpression(); - } else { - $expression = $this->IdentificationVariable(); - } + $expression = ($peek['value'] == '.') + ? $this->StateFieldPathExpression() + : $this->IdentificationVariable(); return new AST\SimpleSelectExpression($expression); } else if ($this->_lexer->lookahead['value'] == '(') { @@ -1964,8 +1986,7 @@ class Parser $this->_lexer->peek(); $expression = $this->ScalarExpression(); - - $expr = new AST\SimpleSelectExpression($expression); + $expr = new AST\SimpleSelectExpression($expression); if ($this->_lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); @@ -1999,6 +2020,7 @@ class Parser while ($this->_lexer->isNextToken(Lexer::T_OR)) { $this->match(Lexer::T_OR); + $conditionalTerms[] = $this->ConditionalTerm(); } @@ -2023,6 +2045,7 @@ class Parser while ($this->_lexer->isNextToken(Lexer::T_AND)) { $this->match(Lexer::T_AND); + $conditionalFactors[] = $this->ConditionalFactor(); } @@ -2046,9 +2069,10 @@ class Parser if ($this->_lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::T_NOT); + $not = true; } - + $conditionalPrimary = $this->ConditionalPrimary(); // Phase 1 AST optimization: Prevent AST\ConditionalFactor @@ -2203,13 +2227,13 @@ class Parser */ public function CollectionMemberExpression() { - $not = false; - + $not = false; $entityExpr = $this->EntityExpression(); if ($this->_lexer->isNextToken(Lexer::T_NOT)) { - $not = true; $this->match(Lexer::T_NOT); + + $not = true; } $this->match(Lexer::T_MEMBER); @@ -2374,7 +2398,7 @@ class Parser $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS); $sign = $isPlus; } - + $primary = $this->ArithmeticPrimary(); // Phase 1 AST optimization: Prevent AST\ArithmeticFactor @@ -2407,7 +2431,7 @@ class Parser case Lexer::T_NULLIF: case Lexer::T_CASE: return $this->CaseExpression(); - + case Lexer::T_IDENTIFIER: $peek = $this->_lexer->glimpse(); @@ -2418,11 +2442,11 @@ class Parser if ($peek['value'] == '.') { return $this->SingleValuedPathExpression(); } - + if (isset($this->_queryComponents[$this->_lexer->lookahead['value']]['resultVariable'])) { return $this->ResultVariable(); } - + return $this->StateFieldPathExpression(); case Lexer::T_INPUT_PARAMETER: @@ -2703,47 +2727,47 @@ class Parser $this->match(Lexer::T_INSTANCE); $this->match(Lexer::T_OF); - + $exprValues = array(); - + if ($this->_lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) { $this->match(Lexer::T_OPEN_PARENTHESIS); - + $exprValues[] = $this->InstanceOfParameter(); while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); - + $exprValues[] = $this->InstanceOfParameter(); } - + $this->match(Lexer::T_CLOSE_PARENTHESIS); - + $instanceOfExpression->value = $exprValues; - + return $instanceOfExpression; } $exprValues[] = $this->InstanceOfParameter(); $instanceOfExpression->value = $exprValues; - + return $instanceOfExpression; } - + /** * InstanceOfParameter ::= AbstractSchemaName | InputParameter - * + * * @return mixed */ public function InstanceOfParameter() { if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { $this->match(Lexer::T_INPUT_PARAMETER); - + return new AST\InputParameter($this->_lexer->token['value']); } - + return $this->AliasIdentificationVariable(); } @@ -2829,8 +2853,10 @@ class Parser $this->match(Lexer::T_EXISTS); $this->match(Lexer::T_OPEN_PARENTHESIS); + $existsExpression = new AST\ExistsExpression($this->Subselect()); $existsExpression->not = $not; + $this->match(Lexer::T_CLOSE_PARENTHESIS); return $existsExpression; @@ -2894,26 +2920,45 @@ class Parser $funcName = strtolower($token['value']); // Check for built-in functions first! - if (isset(self::$_STRING_FUNCTIONS[$funcName])) { - return $this->FunctionsReturningStrings(); - } else if (isset(self::$_NUMERIC_FUNCTIONS[$funcName])) { - return $this->FunctionsReturningNumerics(); - } else if (isset(self::$_DATETIME_FUNCTIONS[$funcName])) { - return $this->FunctionsReturningDatetime(); + switch (true) { + case (isset(self::$_STRING_FUNCTIONS[$funcName])): + return $this->FunctionsReturningStrings(); + + case (isset(self::$_NUMERIC_FUNCTIONS[$funcName])): + return $this->FunctionsReturningNumerics(); + + case (isset(self::$_DATETIME_FUNCTIONS[$funcName])): + return $this->FunctionsReturningDatetime(); + + default: + return $this->CustomFunctionDeclaration(); } + } + + /** + * Helper function for FunctionDeclaration grammar rule + */ + private function CustomFunctionDeclaration() + { + $token = $this->_lexer->lookahead; + $funcName = strtolower($token['value']); // Check for custom functions afterwards $config = $this->_em->getConfiguration(); - if ($config->getCustomStringFunction($funcName) !== null) { - return $this->CustomFunctionsReturningStrings(); - } else if ($config->getCustomNumericFunction($funcName) !== null) { - return $this->CustomFunctionsReturningNumerics(); - } else if ($config->getCustomDatetimeFunction($funcName) !== null) { - return $this->CustomFunctionsReturningDatetime(); - } + switch (true) { + case ($config->getCustomStringFunction($funcName) !== null): + return $this->CustomFunctionsReturningStrings(); - $this->syntaxError('known function', $token); + case ($config->getCustomNumericFunction($funcName) !== null): + return $this->CustomFunctionsReturningNumerics(); + + case ($config->getCustomDatetimeFunction($funcName) !== null): + return $this->CustomFunctionsReturningDatetime(); + + default: + $this->syntaxError('known function', $token); + } } /** @@ -2928,7 +2973,8 @@ class Parser public function FunctionsReturningNumerics() { $funcNameLower = strtolower($this->_lexer->lookahead['value']); - $funcClass = self::$_NUMERIC_FUNCTIONS[$funcNameLower]; + $funcClass = self::$_NUMERIC_FUNCTIONS[$funcNameLower]; + $function = new $funcClass($funcNameLower); $function->parse($this); @@ -2937,9 +2983,10 @@ class Parser public function CustomFunctionsReturningNumerics() { - $funcName = strtolower($this->_lexer->lookahead['value']); // getCustomNumericFunction is case-insensitive + $funcName = strtolower($this->_lexer->lookahead['value']); $funcClass = $this->_em->getConfiguration()->getCustomNumericFunction($funcName); + $function = new $funcClass($funcName); $function->parse($this); @@ -2952,7 +2999,8 @@ class Parser public function FunctionsReturningDatetime() { $funcNameLower = strtolower($this->_lexer->lookahead['value']); - $funcClass = self::$_DATETIME_FUNCTIONS[$funcNameLower]; + $funcClass = self::$_DATETIME_FUNCTIONS[$funcNameLower]; + $function = new $funcClass($funcNameLower); $function->parse($this); @@ -2961,9 +3009,10 @@ class Parser public function CustomFunctionsReturningDatetime() { - $funcName = $this->_lexer->lookahead['value']; // getCustomDatetimeFunction is case-insensitive + $funcName = $this->_lexer->lookahead['value']; $funcClass = $this->_em->getConfiguration()->getCustomDatetimeFunction($funcName); + $function = new $funcClass($funcName); $function->parse($this); @@ -2981,7 +3030,8 @@ class Parser public function FunctionsReturningStrings() { $funcNameLower = strtolower($this->_lexer->lookahead['value']); - $funcClass = self::$_STRING_FUNCTIONS[$funcNameLower]; + $funcClass = self::$_STRING_FUNCTIONS[$funcNameLower]; + $function = new $funcClass($funcNameLower); $function->parse($this); @@ -2990,9 +3040,10 @@ class Parser public function CustomFunctionsReturningStrings() { - $funcName = $this->_lexer->lookahead['value']; // getCustomStringFunction is case-insensitive + $funcName = $this->_lexer->lookahead['value']; $funcClass = $this->_em->getConfiguration()->getCustomStringFunction($funcName); + $function = new $funcClass($funcName); $function->parse($this); diff --git a/lib/Doctrine/ORM/Query/QueryException.php b/lib/Doctrine/ORM/Query/QueryException.php index 39dc42505..b9ab2bb07 100644 --- a/lib/Doctrine/ORM/Query/QueryException.php +++ b/lib/Doctrine/ORM/Query/QueryException.php @@ -47,6 +47,11 @@ class QueryException extends \Doctrine\ORM\ORMException return new self('[Semantical Error] ' . $message); } + public static function invalidLockMode() + { + return new self('Invalid lock mode hint provided.'); + } + public static function invalidParameterType($expected, $received) { return new self('Invalid parameter type, ' . $received . ' given, but ' . $expected . ' expected.'); diff --git a/lib/Doctrine/ORM/Query/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index 3a086e2cd..0d9fed0b9 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMapping.php +++ b/lib/Doctrine/ORM/Query/ResultSetMapping.php @@ -26,7 +26,7 @@ namespace Doctrine\ORM\Query; * The properties of this class are only public for fast internal READ access and to (drastically) * reduce the size of serialized instances for more effective caching due to better (un-)serialization * performance. - * + * * Users should use the public methods. * * @author Roman Borschel @@ -36,87 +36,79 @@ namespace Doctrine\ORM\Query; class ResultSetMapping { /** - * Whether the result is mixed (contains scalar values together with field values). - * * @ignore - * @var boolean + * @var boolean Whether the result is mixed (contains scalar values together with field values). */ public $isMixed = false; + /** - * Maps alias names to class names. - * * @ignore - * @var array + * @var array Maps alias names to class names. */ public $aliasMap = array(); + /** - * Maps alias names to related association field names. - * * @ignore - * @var array + * @var array Maps alias names to related association field names. */ public $relationMap = array(); + /** - * Maps alias names to parent alias names. - * * @ignore - * @var array + * @var array Maps alias names to parent alias names. */ public $parentAliasMap = array(); + /** - * Maps column names in the result set to field names for each class. - * * @ignore - * @var array + * @var array Maps column names in the result set to field names for each class. */ public $fieldMappings = array(); + /** - * Maps column names in the result set to the alias/field name to use in the mapped result. - * * @ignore - * @var array + * @var array Maps column names in the result set to the alias/field name to use in the mapped result. */ public $scalarMappings = array(); + /** - * Maps column names of meta columns (foreign keys, discriminator columns, ...) to field names. - * * @ignore - * @var array + * @var array Maps entities in the result set to the alias name to use in the mapped result. + */ + public $entityMappings = array(); + + /** + * @ignore + * @var array Maps column names of meta columns (foreign keys, discriminator columns, ...) to field names. */ public $metaMappings = array(); + /** - * Maps column names in the result set to the alias they belong to. - * * @ignore - * @var array + * @var array Maps column names in the result set to the alias they belong to. */ public $columnOwnerMap = array(); + /** - * List of columns in the result set that are used as discriminator columns. - * * @ignore - * @var array + * @var array List of columns in the result set that are used as discriminator columns. */ public $discriminatorColumns = array(); + /** - * Maps alias names to field names that should be used for indexing. - * * @ignore - * @var array + * @var array Maps alias names to field names that should be used for indexing. */ public $indexByMap = array(); + /** - * Map from column names to class names that declare the field the column is mapped to. - * * @ignore - * @var array + * @var array Map from column names to class names that declare the field the column is mapped to. */ public $declaringClasses = array(); - + /** - * This is necessary to hydrate derivate foreign keys correctly. - * - * @var array + * @var array This is necessary to hydrate derivate foreign keys correctly. */ public $isIdentifierColumn = array(); @@ -126,11 +118,15 @@ class ResultSetMapping * @param string $class The class name of the entity. * @param string $alias The alias for the class. The alias must be unique among all entity * results or joined entity results within this ResultSetMapping. + * @param string $resultAlias The result alias with which the entity result should be + * placed in the result structure. + * * @todo Rename: addRootEntity */ - public function addEntityResult($class, $alias) + public function addEntityResult($class, $alias, $resultAlias = null) { $this->aliasMap[$alias] = $class; + $this->entityMappings[$alias] = $resultAlias; } /** @@ -141,6 +137,7 @@ class ResultSetMapping * @param string $alias The alias of the entity result or joined entity result the discriminator * column should be used for. * @param string $discrColumn The name of the discriminator column in the SQL result set. + * * @todo Rename: addDiscriminatorColumn */ public function setDiscriminatorColumn($alias, $discrColumn) @@ -157,7 +154,51 @@ class ResultSetMapping */ public function addIndexBy($alias, $fieldName) { - $this->indexByMap[$alias] = $fieldName; + $found = false; + + foreach ($this->fieldMappings AS $columnName => $columnFieldName) { + if ( ! ($columnFieldName === $fieldName && $this->columnOwnerMap[$columnName] === $alias)) continue; + + $this->addIndexByColumn($alias, $columnName); + $found = true; + + break; + } + + /* TODO: check if this exception can be put back, for now it's gone because of assumptions made by some ORM internals + if ( ! $found) { + $message = sprintf( + 'Cannot add index by for DQL alias %s and field %s without calling addFieldResult() for them before.', + $alias, + $fieldName + ); + + throw new \LogicException($message); + } + */ + } + + /** + * Set to index by a scalar result column name + * + * @param $resultColumnName + * @return void + */ + public function addIndexByScalar($resultColumnName) + { + $this->indexByMap['scalars'] = $resultColumnName; + } + + /** + * Sets a column to use for indexing an entity or joined entity result by the given alias name. + * + * @param $alias + * @param $resultColumnName + * @return void + */ + public function addIndexByColumn($alias, $resultColumnName) + { + $this->indexByMap[$alias] = $resultColumnName; } /** @@ -207,6 +248,7 @@ class ResultSetMapping $this->columnOwnerMap[$columnName] = $alias; // field name => class name of declaring class $this->declaringClasses[$columnName] = $declaringClass ?: $this->aliasMap[$alias]; + if ( ! $this->isMixed && $this->scalarMappings) { $this->isMixed = true; } @@ -223,11 +265,11 @@ class ResultSetMapping */ public function addJoinedEntityResult($class, $alias, $parentAlias, $relation) { - $this->aliasMap[$alias] = $class; + $this->aliasMap[$alias] = $class; $this->parentAliasMap[$alias] = $parentAlias; - $this->relationMap[$alias] = $relation; + $this->relationMap[$alias] = $relation; } - + /** * Adds a scalar result mapping. * @@ -238,6 +280,7 @@ class ResultSetMapping public function addScalarResult($columnName, $alias) { $this->scalarMappings[$columnName] = $alias; + if ( ! $this->isMixed && $this->fieldMappings) { $this->isMixed = true; } @@ -245,7 +288,7 @@ class ResultSetMapping /** * Checks whether a column with a given name is mapped as a scalar result. - * + * * @param string $columName The name of the column in the SQL result set. * @return boolean * @todo Rename: isScalar @@ -384,10 +427,10 @@ class ResultSetMapping { return $this->isMixed; } - + /** * Adds a meta column (foreign key or discriminator column) to the result set. - * + * * @param string $alias * @param string $columnName * @param string $fieldName @@ -397,8 +440,9 @@ class ResultSetMapping { $this->metaMappings[$columnName] = $fieldName; $this->columnOwnerMap[$columnName] = $alias; + if ($isIdentifierColumn) { $this->isIdentifierColumn[$alias][$columnName] = true; } } -} \ No newline at end of file +} diff --git a/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php b/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php index d1690b72c..f84686ad2 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php +++ b/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php @@ -86,20 +86,22 @@ class ResultSetMappingBuilder extends ResultSetMapping if (isset($renamedColumns[$columnName])) { $columnName = $renamedColumns[$columnName]; } + $columnName = $platform->getSQLResultCasing($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); + $this->addFieldResult($alias, $columnName, $propertyName); } foreach ($classMetadata->associationMappings AS $associationMapping) { if ($associationMapping['isOwningSide'] && $associationMapping['type'] & ClassMetadataInfo::TO_ONE) { foreach ($associationMapping['joinColumns'] AS $joinColumn) { $columnName = $joinColumn['name']; $renamedColumnName = isset($renamedColumns[$columnName]) ? $renamedColumns[$columnName] : $columnName; + $renamedColumnName = $platform->getSQLResultCasing($renamedColumnName); if (isset($this->metaMappings[$renamedColumnName])) { throw new \InvalidArgumentException("The column '$renamedColumnName' conflicts with another column in the mapper."); } - $this->addMetaResult($alias, $platform->getSQLResultCasing($renamedColumnName), $platform->getSQLResultCasing($columnName)); + $this->addMetaResult($alias, $renamedColumnName, $columnName); } } } diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 6e6f80ce6..ba5d29714 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -28,9 +28,10 @@ use Doctrine\DBAL\LockMode, * The SqlWalker is a TreeWalker that walks over a DQL AST and constructs * the corresponding SQL. * + * @author Guilherme Blanco * @author Roman Borschel * @author Benjamin Eberlei - * @since 2.0 + * @since 2.0 * @todo Rename: SQLWalker */ class SqlWalker implements TreeWalker @@ -71,6 +72,13 @@ class SqlWalker implements TreeWalker /** Map from result variable names to their SQL column alias names. */ private $_scalarResultAliasMap = array(); + /** + * Map from DQL-Alias + Field-Name to SQL Column Alias + * + * @var array + */ + private $_scalarFields = array(); + /** Map of all components/classes that appear in the DQL query. */ private $_queryComponents; @@ -160,18 +168,18 @@ class SqlWalker implements TreeWalker switch (true) { case ($AST instanceof AST\DeleteStatement): $primaryClass = $this->_em->getClassMetadata($AST->deleteClause->abstractSchemaName); - + return ($primaryClass->isInheritanceTypeJoined()) ? new Exec\MultiTableDeleteExecutor($AST, $this) : new Exec\SingleTableDeleteUpdateExecutor($AST, $this); - + case ($AST instanceof AST\UpdateStatement): $primaryClass = $this->_em->getClassMetadata($AST->updateClause->abstractSchemaName); - - return ($primaryClass->isInheritanceTypeJoined()) + + return ($primaryClass->isInheritanceTypeJoined()) ? new Exec\MultiTableUpdateExecutor($AST, $this) : new Exec\SingleTableDeleteUpdateExecutor($AST, $this); - + default: return new Exec\SingleSelectExecutor($AST, $this); } @@ -221,7 +229,11 @@ class SqlWalker implements TreeWalker */ public function getSQLColumnAlias($columnName) { - return $columnName . $this->_aliasCounter++; + // Trim the column alias to the maximum identifier length of the platform. + // If the alias is to long, characters are cut off from the beginning. + return $this->_platform->getSQLResultCasing( + substr($columnName . $this->_aliasCounter++, -$this->_platform->getMaxIdentifierLength()) + ); } /** @@ -241,36 +253,40 @@ class SqlWalker implements TreeWalker // INNER JOIN parent class tables foreach ($class->parentClasses as $parentClassName) { $parentClass = $this->_em->getClassMetadata($parentClassName); - $tableAlias = $this->getSQLTableAlias($parentClass->getTableName(), $dqlAlias); - + $tableAlias = $this->getSQLTableAlias($parentClass->getTableName(), $dqlAlias); + // If this is a joined association we must use left joins to preserve the correct result. $sql .= isset($this->_queryComponents[$dqlAlias]['relation']) ? ' LEFT ' : ' INNER '; $sql .= 'JOIN ' . $parentClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON '; - $first = true; - - foreach ($class->identifier as $idField) { - if ($first) $first = false; else $sql .= ' AND '; - $columnName = $class->getQuotedColumnName($idField, $this->_platform); - $sql .= $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; + $sqlParts = array(); + + foreach ($class->getQuotedIdentifierColumnNames($this->_platform) as $columnName) { + $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; } + + $sql .= implode(' AND ', $sqlParts); } - // LEFT JOIN subclass tables, if partial objects disallowed. - if ( ! $this->_query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) { - foreach ($class->subClasses as $subClassName) { - $subClass = $this->_em->getClassMetadata($subClassName); - $tableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); - $sql .= ' LEFT JOIN ' . $subClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON '; - $first = true; - - foreach ($class->identifier as $idField) { - if ($first) $first = false; else $sql .= ' AND '; + // Ignore subclassing inclusion if partial objects is disallowed + if ($this->_query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) { + return $sql; + } - $columnName = $class->getQuotedColumnName($idField, $this->_platform); - $sql .= $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; - } + // LEFT JOIN child class tables + foreach ($class->subClasses as $subClassName) { + $subClass = $this->_em->getClassMetadata($subClassName); + $tableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); + + $sql .= ' LEFT JOIN ' . $subClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON '; + + $sqlParts = array(); + + foreach ($subClass->getQuotedIdentifierColumnNames($this->_platform) as $columnName) { + $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; } + + $sql .= implode(' AND ', $sqlParts); } return $sql; @@ -278,28 +294,25 @@ class SqlWalker implements TreeWalker private function _generateOrderedCollectionOrderByItems() { - $sql = ''; - - foreach ($this->_selectedClasses AS $dqlAlias => $class) { - $qComp = $this->_queryComponents[$dqlAlias]; - - if (isset($qComp['relation']['orderBy'])) { - foreach ($qComp['relation']['orderBy'] AS $fieldName => $orientation) { - $tableName = ($qComp['metadata']->isInheritanceTypeJoined()) - ? $this->_em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName) - : $qComp['metadata']->getTableName(); + $sqlParts = array(); - if ($sql != '') { - $sql .= ', '; - } - - $sql .= $this->getSQLTableAlias($tableName, $dqlAlias) . '.' - . $qComp['metadata']->getQuotedColumnName($fieldName, $this->_platform) . ' ' . $orientation; - } + foreach ($this->_selectedClasses AS $selectedClass) { + $dqlAlias = $selectedClass['dqlAlias']; + $qComp = $this->_queryComponents[$dqlAlias]; + + if ( ! isset($qComp['relation']['orderBy'])) continue; + + foreach ($qComp['relation']['orderBy'] AS $fieldName => $orientation) { + $columnName = $qComp['metadata']->getQuotedColumnName($fieldName, $this->_platform); + $tableName = ($qComp['metadata']->isInheritanceTypeJoined()) + ? $this->_em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name)->getOwningTable($fieldName) + : $qComp['metadata']->getTableName(); + + $sqlParts[] = $this->getSQLTableAlias($tableName, $dqlAlias) . '.' . $columnName . ' ' . $orientation; } } - - return $sql; + + return implode(', ', $sqlParts); } /** @@ -310,36 +323,31 @@ class SqlWalker implements TreeWalker */ private function _generateDiscriminatorColumnConditionSQL(array $dqlAliases) { - $encapsulate = false; - $sql = ''; + $sqlParts = array(); foreach ($dqlAliases as $dqlAlias) { $class = $this->_queryComponents[$dqlAlias]['metadata']; - if ($class->isInheritanceTypeSingleTable()) { - $conn = $this->_em->getConnection(); - $values = array(); - - if ($class->discriminatorValue !== null) { // discrimnators can be 0 - $values[] = $conn->quote($class->discriminatorValue); - } + if ( ! $class->isInheritanceTypeSingleTable()) continue; - foreach ($class->subClasses as $subclassName) { - $values[] = $conn->quote($this->_em->getClassMetadata($subclassName)->discriminatorValue); - } + $conn = $this->_em->getConnection(); + $values = array(); - if ($sql != '') { - $sql .= ' AND '; - $encapsulate = true; - } - - $sql .= ($sql != '' ? ' AND ' : '') - . (($this->_useSqlTableAliases) ? $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.' : '') - . $class->discriminatorColumn['name'] . ' IN (' . implode(', ', $values) . ')'; + if ($class->discriminatorValue !== null) { // discrimnators can be 0 + $values[] = $conn->quote($class->discriminatorValue); } + + foreach ($class->subClasses as $subclassName) { + $values[] = $conn->quote($this->_em->getClassMetadata($subclassName)->discriminatorValue); + } + + $sqlParts[] = (($this->_useSqlTableAliases) ? $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.' : '') + . $class->discriminatorColumn['name'] . ' IN (' . implode(', ', $values) . ')'; } - return ($encapsulate) ? '(' . $sql . ')' : $sql; + $sql = implode(' AND ', $sqlParts); + + return (count($sqlParts) > 1) ? '(' . $sql . ')' : $sql; } /** @@ -366,16 +374,25 @@ class SqlWalker implements TreeWalker ); if (($lockMode = $this->_query->getHint(Query::HINT_LOCK_MODE)) !== false) { - if ($lockMode == LockMode::PESSIMISTIC_READ) { - $sql .= ' ' . $this->_platform->getReadLockSQL(); - } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) { - $sql .= ' ' . $this->_platform->getWriteLockSQL(); - } else if ($lockMode == LockMode::OPTIMISTIC) { - foreach ($this->_selectedClasses AS $class) { - if ( ! $class->isVersioned) { - throw \Doctrine\ORM\OptimisticLockException::lockFailed(); + switch ($lockMode) { + case LockMode::PESSIMISTIC_READ: + $sql .= ' ' . $this->_platform->getReadLockSQL(); + break; + + case LockMode::PESSIMISTIC_WRITE: + $sql .= ' ' . $this->_platform->getWriteLockSQL(); + break; + + case LockMode::PESSIMISTIC_OPTIMISTIC: + foreach ($this->_selectedClasses AS $selectedClass) { + if ( ! $class->isVersioned) { + throw \Doctrine\ORM\OptimisticLockException::lockFailed($selectedClass['class']->name); + } } - } + break; + + default: + throw \Doctrine\ORM\Query\QueryException::invalidLockMode(); } } @@ -391,7 +408,7 @@ class SqlWalker implements TreeWalker public function walkUpdateStatement(AST\UpdateStatement $AST) { $this->_useSqlTableAliases = false; - + return $this->walkUpdateClause($AST->updateClause) . $this->walkWhereClause($AST->whereClause); } @@ -404,7 +421,7 @@ class SqlWalker implements TreeWalker public function walkDeleteStatement(AST\DeleteStatement $AST) { $this->_useSqlTableAliases = false; - + return $this->walkDeleteClause($AST->deleteClause) . $this->walkWhereClause($AST->whereClause); } @@ -469,7 +486,7 @@ class SqlWalker implements TreeWalker if ( ! $assoc['isOwningSide']) { throw QueryException::associationPathInverseSideNotSupported(); } - + // COMPOSITE KEYS NOT (YET?) SUPPORTED if (count($assoc['sourceToTargetKeyColumns']) > 1) { throw QueryException::associationPathCompositeKeyNotSupported(); @@ -481,7 +498,7 @@ class SqlWalker implements TreeWalker $sql .= reset($assoc['targetToSourceKeyColumns']); break; - + default: throw QueryException::invalidPathExpression($pathExpr); } @@ -506,13 +523,18 @@ class SqlWalker implements TreeWalker $this->_query->getHydrationMode() != Query::HYDRATE_OBJECT && $this->_query->getHint(Query::HINT_INCLUDE_META_COLUMNS); - foreach ($this->_selectedClasses as $dqlAlias => $class) { + foreach ($this->_selectedClasses as $selectedClass) { + $class = $selectedClass['class']; + $dqlAlias = $selectedClass['dqlAlias']; + $resultAlias = $selectedClass['resultAlias']; + // Register as entity or joined entity result if ($this->_queryComponents[$dqlAlias]['relation'] === null) { - $this->_rsm->addEntityResult($class->name, $dqlAlias); + $this->_rsm->addEntityResult($class->name, $dqlAlias, $resultAlias); } else { $this->_rsm->addJoinedEntityResult( - $class->name, $dqlAlias, + $class->name, + $dqlAlias, $this->_queryComponents[$dqlAlias]['parent'], $this->_queryComponents[$dqlAlias]['relation']['fieldName'] ); @@ -520,61 +542,58 @@ class SqlWalker implements TreeWalker if ($class->isInheritanceTypeSingleTable() || $class->isInheritanceTypeJoined()) { // Add discriminator columns to SQL - $rootClass = $this->_em->getClassMetadata($class->rootEntityName); - $tblAlias = $this->getSQLTableAlias($rootClass->getTableName(), $dqlAlias); + $rootClass = $this->_em->getClassMetadata($class->rootEntityName); + $tblAlias = $this->getSQLTableAlias($rootClass->getTableName(), $dqlAlias); $discrColumn = $rootClass->discriminatorColumn; $columnAlias = $this->getSQLColumnAlias($discrColumn['name']); - + $sqlSelectExpressions[] = $tblAlias . '.' . $discrColumn['name'] . ' AS ' . $columnAlias; - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); $this->_rsm->setDiscriminatorColumn($dqlAlias, $columnAlias); $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $discrColumn['fieldName']); + } - // Add foreign key columns to SQL, if necessary - if ($addMetaColumns) { - //FIXME: Include foreign key columns of child classes also!!?? - foreach ($class->associationMappings as $assoc) { - if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { - if (isset($assoc['inherited'])) { - $owningClass = $this->_em->getClassMetadata($assoc['inherited']); - $sqlTableAlias = $this->getSQLTableAlias($owningClass->getTableName(), $dqlAlias); - } else { - $sqlTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); - } - - foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { - $columnAlias = $this->getSQLColumnAlias($srcColumn); - - $sqlSelectExpressions[] = $sqlTableAlias . '.' . $srcColumn . ' AS ' . $columnAlias; - - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - $this->_rsm->addMetaResult($dqlAlias, $this->_platform->getSQLResultCasing($columnAlias), $srcColumn, (isset($assoc['id']) && $assoc['id'] === true)); - } - } - } + // Add foreign key columns to SQL, if necessary + if ( ! $addMetaColumns) continue; + + // Add foreign key columns of class and also parent classes + foreach ($class->associationMappings as $assoc) { + if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) continue; + + $owningClass = (isset($assoc['inherited'])) ? $this->_em->getClassMetadata($assoc['inherited']) : $class; + $sqlTableAlias = $this->getSQLTableAlias($owningClass->getTableName(), $dqlAlias); + + foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { + $columnAlias = $this->getSQLColumnAlias($srcColumn); + + $sqlSelectExpressions[] = $sqlTableAlias . '.' . $srcColumn . ' AS ' . $columnAlias; + + $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $srcColumn, (isset($assoc['id']) && $assoc['id'] === true)); } - } else { - // Add foreign key columns to SQL, if necessary - if ($addMetaColumns) { - $sqlTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); - - foreach ($class->associationMappings as $assoc) { - if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { - foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { - $columnAlias = $this->getSQLColumnAlias($srcColumn); - - $sqlSelectExpressions[] = $sqlTableAlias . '.' . $srcColumn . ' AS ' . $columnAlias; - - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - $this->_rsm->addMetaResult($dqlAlias, $this->_platform->getSQLResultCasing($columnAlias), $srcColumn, (isset($assoc['id']) && $assoc['id'] === true)); - } - } + } + + // Add foreign key columns of subclasses + foreach ($class->subClasses as $subClassName) { + $subClass = $this->_em->getClassMetadata($subClassName); + $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); + + foreach ($subClass->associationMappings as $assoc) { + // Skip if association is inherited + if (isset($assoc['inherited'])) continue; + + if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) continue; + + foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { + $columnAlias = $this->getSQLColumnAlias($srcColumn); + + $sqlSelectExpressions[] = $sqlTableAlias . '.' . $srcColumn . ' AS ' . $columnAlias; + + $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $srcColumn); } } } } - + $sql .= implode(', ', $sqlSelectExpressions); return $sql; @@ -595,7 +614,7 @@ class SqlWalker implements TreeWalker $rangeDecl = $identificationVariableDecl->rangeVariableDeclaration; $dqlAlias = $rangeDecl->aliasIdentificationVariable; - + $this->_rootAliases[] = $dqlAlias; $class = $this->_em->getClassMetadata($rangeDecl->abstractSchemaName); @@ -611,10 +630,17 @@ class SqlWalker implements TreeWalker } if ($identificationVariableDecl->indexBy) { - $this->_rsm->addIndexBy( - $identificationVariableDecl->indexBy->simpleStateFieldPathExpression->identificationVariable, - $identificationVariableDecl->indexBy->simpleStateFieldPathExpression->field - ); + $alias = $identificationVariableDecl->indexBy->simpleStateFieldPathExpression->identificationVariable; + $field = $identificationVariableDecl->indexBy->simpleStateFieldPathExpression->field; + + if (isset($this->_scalarFields[$alias][$field])) { + $this->_rsm->addIndexByScalar($this->_scalarFields[$alias][$field]); + } else { + $this->_rsm->addIndexBy( + $identificationVariableDecl->indexBy->simpleStateFieldPathExpression->identificationVariable, + $identificationVariableDecl->indexBy->simpleStateFieldPathExpression->field + ); + } } $sqlParts[] = $this->_platform->appendLockHint($sql, $this->_query->getHint(Query::HINT_LOCK_MODE)); @@ -641,15 +667,13 @@ class SqlWalker implements TreeWalker */ public function walkOrderByClause($orderByClause) { - $colSql = $this->_generateOrderedCollectionOrderByItems(); - if ($colSql != '') { - $colSql = ", ".$colSql; + $orderByItems = array_map(array($this, 'walkOrderByItem'), $orderByClause->orderByItems); + + if (($collectionOrderByItems = $this->_generateOrderedCollectionOrderByItems()) !== '') { + $orderByItems = array_merge($orderByItems, (array) $collectionOrderByItems); } - // OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}* - return ' ORDER BY ' . implode( - ', ', array_map(array($this, 'walkOrderByItem'), $orderByClause->orderByItems) - ) . $colSql; + return ' ORDER BY ' . implode(', ', $orderByItems); } /** @@ -660,16 +684,10 @@ class SqlWalker implements TreeWalker */ public function walkOrderByItem($orderByItem) { - $sql = ''; $expr = $orderByItem->expression; - - if ($expr instanceof AST\PathExpression) { - $sql = $this->walkPathExpression($expr); - } else { - $columnName = $this->_queryComponents[$expr]['token']['value']; - - $sql = $this->_scalarResultAliasMap[$columnName]; - } + $sql = ($expr instanceof AST\PathExpression) + ? $this->walkPathExpression($expr) + : $this->_scalarResultAliasMap[$this->_queryComponents[$expr]['token']['value']]; return $sql . ' ' . strtoupper($orderByItem->type); } @@ -693,8 +711,11 @@ class SqlWalker implements TreeWalker */ public function walkJoinVariableDeclaration($joinVarDecl) { - $join = $joinVarDecl->join; + $join = $joinVarDecl->join; $joinType = $join->joinType; + $sql = ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) + ? ' LEFT JOIN ' + : ' INNER JOIN '; if ($joinVarDecl->indexBy) { // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. @@ -704,30 +725,22 @@ class SqlWalker implements TreeWalker ); } - if ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) { - $sql = ' LEFT JOIN '; - } else { - $sql = ' INNER JOIN '; - } - $joinAssocPathExpr = $join->joinAssociationPathExpression; $joinedDqlAlias = $join->aliasIdentificationVariable; - + $relation = $this->_queryComponents[$joinedDqlAlias]['relation']; $targetClass = $this->_em->getClassMetadata($relation['targetEntity']); $sourceClass = $this->_em->getClassMetadata($relation['sourceEntity']); $targetTableName = $targetClass->getQuotedTableName($this->_platform); - + $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias); $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $joinAssocPathExpr->identificationVariable); // Ensure we got the owning side, since it has all mapping info $assoc = ( ! $relation['isOwningSide']) ? $targetClass->associationMappings[$relation['mappedBy']] : $relation; - if ($this->_query->getHint(Query::HINT_INTERNAL_ITERATION) == true) { - if ($relation['type'] == ClassMetadata::ONE_TO_MANY || $relation['type'] == ClassMetadata::MANY_TO_MANY) { - throw QueryException::iterateWithFetchJoinNotAllowed($assoc); - } + if ($this->_query->getHint(Query::HINT_INTERNAL_ITERATION) == true && $relation['type'] & ClassMetadata::TO_MANY) { + throw QueryException::iterateWithFetchJoinNotAllowed($assoc); } if ($joinVarDecl->indexBy) { @@ -744,7 +757,6 @@ class SqlWalker implements TreeWalker // be the owning side and previously we ensured that $assoc is always the owning side of the associations. // The owning side is necessary at this point because only it contains the JoinColumn information. if ($assoc['type'] & ClassMetadata::TO_ONE) { - $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON '; $first = true; @@ -852,7 +864,7 @@ class SqlWalker implements TreeWalker return $sql; } - + /** * Walks down a CaseExpression AST node and generates the corresponding SQL. * @@ -864,21 +876,21 @@ class SqlWalker implements TreeWalker switch (true) { case ($expression instanceof AST\CoalesceExpression): return $this->walkCoalesceExpression($expression); - + case ($expression instanceof AST\NullIfExpression): return $this->walkNullIfExpression($expression); - + case ($expression instanceof AST\GeneralCaseExpression): return $this->walkGeneralCaseExpression($expression); - + case ($expression instanceof AST\SimpleCaseExpression): return $this->walkSimpleCaseExpression($expression); - + default: return ''; } } - + /** * Walks down a CoalesceExpression AST node and generates the corresponding SQL. * @@ -888,18 +900,18 @@ class SqlWalker implements TreeWalker public function walkCoalesceExpression($coalesceExpression) { $sql = 'COALESCE('; - + $scalarExpressions = array(); - + foreach ($coalesceExpression->scalarExpressions as $scalarExpression) { $scalarExpressions[] = $this->walkSimpleArithmeticExpression($scalarExpression); } - + $sql .= implode(', ', $scalarExpressions) . ')'; - + return $sql; } - + /** * Walks down a NullIfExpression AST node and generates the corresponding SQL. * @@ -908,17 +920,17 @@ class SqlWalker implements TreeWalker */ public function walkNullIfExpression($nullIfExpression) { - $firstExpression = is_string($nullIfExpression->firstExpression) + $firstExpression = is_string($nullIfExpression->firstExpression) ? $this->_conn->quote($nullIfExpression->firstExpression) : $this->walkSimpleArithmeticExpression($nullIfExpression->firstExpression); - - $secondExpression = is_string($nullIfExpression->secondExpression) + + $secondExpression = is_string($nullIfExpression->secondExpression) ? $this->_conn->quote($nullIfExpression->secondExpression) : $this->walkSimpleArithmeticExpression($nullIfExpression->secondExpression); - + return 'NULLIF(' . $firstExpression . ', ' . $secondExpression . ')'; } - + /** * Walks down a GeneralCaseExpression AST node and generates the corresponding SQL. * @@ -928,17 +940,17 @@ class SqlWalker implements TreeWalker public function walkGeneralCaseExpression(AST\GeneralCaseExpression $generalCaseExpression) { $sql = 'CASE'; - + foreach ($generalCaseExpression->whenClauses as $whenClause) { $sql .= ' WHEN ' . $this->walkConditionalExpression($whenClause->caseConditionExpression); $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($whenClause->thenScalarExpression); } - + $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($generalCaseExpression->elseScalarExpression) . ' END'; - + return $sql; } - + /** * Walks down a SimpleCaseExpression AST node and generates the corresponding SQL. * @@ -948,14 +960,14 @@ class SqlWalker implements TreeWalker public function walkSimpleCaseExpression($simpleCaseExpression) { $sql = 'CASE ' . $this->walkStateFieldPathExpression($simpleCaseExpression->caseOperand); - + foreach ($simpleCaseExpression->simpleWhenClauses as $simpleWhenClause) { $sql .= ' WHEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->caseScalarExpression); $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->thenScalarExpression); } - + $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($simpleCaseExpression->elseScalarExpression) . ' END'; - + return $sql; } @@ -971,218 +983,138 @@ class SqlWalker implements TreeWalker $expr = $selectExpression->expression; $hidden = $selectExpression->hiddenAliasResultVariable; - if ($expr instanceof AST\PathExpression) { - if ($expr->type !== AST\PathExpression::TYPE_STATE_FIELD) { - throw QueryException::invalidPathExpression($expr->type); - } - - $fieldName = $expr->field; - $dqlAlias = $expr->identificationVariable; - $qComp = $this->_queryComponents[$dqlAlias]; - $class = $qComp['metadata']; - - if ( ! $selectExpression->fieldIdentificationVariable) { - $resultAlias = $fieldName; - } else { - $resultAlias = $selectExpression->fieldIdentificationVariable; - } - - if ($class->isInheritanceTypeJoined()) { - $tableName = $this->_em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName); - } else { - $tableName = $class->getTableName(); - } - - $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); - $columnName = $class->getQuotedColumnName($fieldName, $this->_platform); - - $columnAlias = $this->getSQLColumnAlias($columnName); - $sql .= $sqlTableAlias . '.' . $columnName . ' AS ' . $columnAlias; - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - - if ( ! $hidden) { - $this->_rsm->addScalarResult($columnAlias, $resultAlias); - } - } else if ($expr instanceof AST\AggregateExpression) { - if ( ! $selectExpression->fieldIdentificationVariable) { - $resultAlias = $this->_scalarResultCounter++; - } else { - $resultAlias = $selectExpression->fieldIdentificationVariable; - } - - $columnAlias = 'sclr' . $this->_aliasCounter++; - $sql .= $this->walkAggregateExpression($expr) . ' AS ' . $columnAlias; - $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; - - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - - if ( ! $hidden) { - $this->_rsm->addScalarResult($columnAlias, $resultAlias); - } - } else if ($expr instanceof AST\Subselect) { - if ( ! $selectExpression->fieldIdentificationVariable) { - $resultAlias = $this->_scalarResultCounter++; - } else { - $resultAlias = $selectExpression->fieldIdentificationVariable; - } - - $columnAlias = 'sclr' . $this->_aliasCounter++; - $sql .= '(' . $this->walkSubselect($expr) . ') AS '.$columnAlias; - $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; - - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - - if ( ! $hidden) { - $this->_rsm->addScalarResult($columnAlias, $resultAlias); - } - } else if ($expr instanceof AST\Functions\FunctionNode) { - if ( ! $selectExpression->fieldIdentificationVariable) { - $resultAlias = $this->_scalarResultCounter++; - } else { - $resultAlias = $selectExpression->fieldIdentificationVariable; - } - - $columnAlias = 'sclr' . $this->_aliasCounter++; - $sql .= $this->walkFunction($expr) . ' AS ' . $columnAlias; - $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; - - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - - if ( ! $hidden) { - $this->_rsm->addScalarResult($columnAlias, $resultAlias); - } - } else if ( - $expr instanceof AST\SimpleArithmeticExpression || - $expr instanceof AST\ArithmeticTerm || - $expr instanceof AST\ArithmeticFactor || - $expr instanceof AST\ArithmeticPrimary || - $expr instanceof AST\Literal - ) { - if ( ! $selectExpression->fieldIdentificationVariable) { - $resultAlias = $this->_scalarResultCounter++; - } else { - $resultAlias = $selectExpression->fieldIdentificationVariable; - } - - $columnAlias = 'sclr' . $this->_aliasCounter++; - - 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); - - if ( ! $hidden) { - $this->_rsm->addScalarResult($columnAlias, $resultAlias); - } - } else if ( - $expr instanceof AST\NullIfExpression || - $expr instanceof AST\CoalesceExpression || - $expr instanceof AST\GeneralCaseExpression || - $expr instanceof AST\SimpleCaseExpression - ) { - if ( ! $selectExpression->fieldIdentificationVariable) { - $resultAlias = $this->_scalarResultCounter++; - } else { - $resultAlias = $selectExpression->fieldIdentificationVariable; - } - - $columnAlias = 'sclr' . $this->_aliasCounter++; - - $sql .= $this->walkCaseExpression($expr) . ' AS ' . $columnAlias; - - $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; - - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - - if ( ! $hidden) { - $this->_rsm->addScalarResult($columnAlias, $resultAlias); - } - } else { - // IdentificationVariable or PartialObjectExpression - if ($expr instanceof AST\PartialObjectExpression) { - $dqlAlias = $expr->identificationVariable; - $partialFieldSet = $expr->partialFieldSet; - } else { - $dqlAlias = $expr; - $partialFieldSet = array(); - } - - $queryComp = $this->_queryComponents[$dqlAlias]; - $class = $queryComp['metadata']; - - if ( ! isset($this->_selectedClasses[$dqlAlias])) { - $this->_selectedClasses[$dqlAlias] = $class; - } - - $beginning = true; - - // Select all fields from the queried class - foreach ($class->fieldMappings as $fieldName => $mapping) { - if ($partialFieldSet && !in_array($fieldName, $partialFieldSet)) { - continue; + switch (true) { + case ($expr instanceof AST\PathExpression): + if ($expr->type !== AST\PathExpression::TYPE_STATE_FIELD) { + throw QueryException::invalidPathExpression($expr->type); } - $tableName = (isset($mapping['inherited'])) - ? $this->_em->getClassMetadata($mapping['inherited'])->getTableName() + $fieldName = $expr->field; + $dqlAlias = $expr->identificationVariable; + $qComp = $this->_queryComponents[$dqlAlias]; + $class = $qComp['metadata']; + + $resultAlias = $selectExpression->fieldIdentificationVariable ?: $fieldName; + $tableName = ($class->isInheritanceTypeJoined()) + ? $this->_em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName) : $class->getTableName(); - if ($beginning) $beginning = false; else $sql .= ', '; - $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); - $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); - $sql .= $sqlTableAlias . '.' . $class->getQuotedColumnName($fieldName, $this->_platform) - . ' AS ' . $columnAlias; + $columnName = $class->getQuotedColumnName($fieldName, $this->_platform); + $columnAlias = $this->getSQLColumnAlias($columnName); - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - - $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); - } + $sql .= $sqlTableAlias . '.' . $columnName . ' AS ' . $columnAlias; - // Add any additional fields of subclasses (excluding inherited fields) - // 1) on Single Table Inheritance: always, since its marginal overhead - // 2) on Class Table Inheritance only if partial objects are disallowed, - // since it requires outer joining subtables. - if ($class->isInheritanceTypeSingleTable() || ! $this->_query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) { - foreach ($class->subClasses as $subClassName) { - $subClass = $this->_em->getClassMetadata($subClassName); - $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); - - foreach ($subClass->fieldMappings as $fieldName => $mapping) { - if (isset($mapping['inherited']) || $partialFieldSet && !in_array($fieldName, $partialFieldSet)) { - continue; - } + if ( ! $hidden) { + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + $this->_scalarFields[$dqlAlias][$fieldName] = $columnAlias; + } + break; - if ($beginning) $beginning = false; else $sql .= ', '; + case ($expr instanceof AST\AggregateExpression): + case ($expr instanceof AST\Functions\FunctionNode): + case ($expr instanceof AST\SimpleArithmeticExpression): + case ($expr instanceof AST\ArithmeticTerm): + case ($expr instanceof AST\ArithmeticFactor): + case ($expr instanceof AST\ArithmeticPrimary): + case ($expr instanceof AST\Literal): + case ($expr instanceof AST\NullIfExpression): + case ($expr instanceof AST\CoalesceExpression): + case ($expr instanceof AST\GeneralCaseExpression): + case ($expr instanceof AST\SimpleCaseExpression): + $columnAlias = $this->getSQLColumnAlias('sclr'); + $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->_scalarResultCounter++; - $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); - $sql .= $sqlTableAlias . '.' . $subClass->getQuotedColumnName($fieldName, $this->_platform) - . ' AS ' . $columnAlias; + $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); + $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; + + if ( ! $hidden) { + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } + break; + + case ($expr instanceof AST\Subselect): + $columnAlias = $this->getSQLColumnAlias('sclr'); + $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->_scalarResultCounter++; + + $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; + + $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; + + if ( ! $hidden) { + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } + break; + + default: + // IdentificationVariable or PartialObjectExpression + if ($expr instanceof AST\PartialObjectExpression) { + $dqlAlias = $expr->identificationVariable; + $partialFieldSet = $expr->partialFieldSet; + } else { + $dqlAlias = $expr; + $partialFieldSet = array(); + } + + $queryComp = $this->_queryComponents[$dqlAlias]; + $class = $queryComp['metadata']; + $resultAlias = $selectExpression->fieldIdentificationVariable ?: null; + + if ( ! isset($this->_selectedClasses[$dqlAlias])) { + $this->_selectedClasses[$dqlAlias] = array( + 'class' => $class, + 'dqlAlias' => $dqlAlias, + 'resultAlias' => $resultAlias + ); + } + + $sqlParts = array(); + + // Select all fields from the queried class + foreach ($class->fieldMappings as $fieldName => $mapping) { + if ($partialFieldSet && ! in_array($fieldName, $partialFieldSet)) { + continue; } - // Add join columns (foreign keys) of the subclass - //TODO: Probably better do this in walkSelectClause to honor the INCLUDE_META_COLUMNS hint - foreach ($subClass->associationMappings as $fieldName => $assoc) { - if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE && ! isset($assoc['inherited'])) { - foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { - if ($beginning) $beginning = false; else $sql .= ', '; - - $columnAlias = $this->getSQLColumnAlias($srcColumn); - $sql .= $sqlTableAlias . '.' . $srcColumn . ' AS ' . $columnAlias; - - $this->_rsm->addMetaResult($dqlAlias, $this->_platform->getSQLResultCasing($columnAlias), $srcColumn); + $tableName = (isset($mapping['inherited'])) + ? $this->_em->getClassMetadata($mapping['inherited'])->getTableName() + : $class->getTableName(); + + $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); + $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); + $quotedColumnName = $class->getQuotedColumnName($fieldName, $this->_platform); + + $sqlParts[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS '. $columnAlias; + + $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); + } + + // Add any additional fields of subclasses (excluding inherited fields) + // 1) on Single Table Inheritance: always, since its marginal overhead + // 2) on Class Table Inheritance only if partial objects are disallowed, + // since it requires outer joining subtables. + if ($class->isInheritanceTypeSingleTable() || ! $this->_query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) { + foreach ($class->subClasses as $subClassName) { + $subClass = $this->_em->getClassMetadata($subClassName); + $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); + + foreach ($subClass->fieldMappings as $fieldName => $mapping) { + if (isset($mapping['inherited']) || $partialFieldSet && !in_array($fieldName, $partialFieldSet)) { + continue; } + + $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); + $quotedColumnName = $subClass->getQuotedColumnName($fieldName, $this->_platform); + + $sqlParts[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; + + $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); } } } - } + + $sql .= implode(', ', $sqlParts); } return $sql; @@ -1196,8 +1128,7 @@ class SqlWalker implements TreeWalker */ public function walkQuantifiedExpression($qExpr) { - return ' ' . strtoupper($qExpr->type) - . '(' . $this->walkSubselect($qExpr->subselect) . ')'; + return ' ' . strtoupper($qExpr->type) . '(' . $this->walkSubselect($qExpr->subselect) . ')'; } /** @@ -1208,20 +1139,21 @@ class SqlWalker implements TreeWalker */ public function walkSubselect($subselect) { - $useAliasesBefore = $this->_useSqlTableAliases; + $useAliasesBefore = $this->_useSqlTableAliases; $rootAliasesBefore = $this->_rootAliases; $this->_rootAliases = array(); // reset the rootAliases for the subselect $this->_useSqlTableAliases = true; - $sql = $this->walkSimpleSelectClause($subselect->simpleSelectClause); + $sql = $this->walkSimpleSelectClause($subselect->simpleSelectClause); $sql .= $this->walkSubselectFromClause($subselect->subselectFromClause); $sql .= $this->walkWhereClause($subselect->whereClause); + $sql .= $subselect->groupByClause ? $this->walkGroupByClause($subselect->groupByClause) : ''; $sql .= $subselect->havingClause ? $this->walkHavingClause($subselect->havingClause) : ''; $sql .= $subselect->orderByClause ? $this->walkOrderByClause($subselect->orderByClause) : ''; - $this->_rootAliases = $rootAliasesBefore; // put the main aliases back + $this->_rootAliases = $rootAliasesBefore; // put the main aliases back $this->_useSqlTableAliases = $useAliasesBefore; return $sql; @@ -1242,7 +1174,7 @@ class SqlWalker implements TreeWalker $sql = ''; $rangeDecl = $subselectIdVarDecl->rangeVariableDeclaration; - $dqlAlias = $rangeDecl->aliasIdentificationVariable; + $dqlAlias = $rangeDecl->aliasIdentificationVariable; $class = $this->_em->getClassMetadata($rangeDecl->abstractSchemaName); $sql .= $class->getQuotedTableName($this->_platform) . ' ' @@ -1286,74 +1218,58 @@ class SqlWalker implements TreeWalker { $expr = $simpleSelectExpression->expression; $sql = ' '; - + switch (true) { case ($expr instanceof AST\PathExpression): $sql .= $this->walkPathExpression($expr); break; - + case ($expr instanceof AST\AggregateExpression): $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->_scalarResultCounter++; - + $sql .= $this->walkAggregateExpression($expr) . ' AS dctrn__' . $alias; break; - + case ($expr instanceof AST\Subselect): $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->_scalarResultCounter++; - + $columnAlias = 'sclr' . $this->_aliasCounter++; $this->_scalarResultAliasMap[$alias] = $columnAlias; - + $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; break; - + case ($expr instanceof AST\Functions\FunctionNode): - $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->_scalarResultCounter++; - - $columnAlias = 'sclr' . $this->_aliasCounter++; - $this->_scalarResultAliasMap[$alias] = $columnAlias; - - $sql .= $this->walkFunction($expr) . ' AS ' . $columnAlias; - break; - case ($expr instanceof AST\SimpleArithmeticExpression): case ($expr instanceof AST\ArithmeticTerm): case ($expr instanceof AST\ArithmeticFactor): case ($expr instanceof AST\ArithmeticPrimary): case ($expr instanceof AST\Literal): - $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->_scalarResultCounter++; - - $columnAlias = 'sclr' . $this->_aliasCounter++; - $this->_scalarResultAliasMap[$alias] = $columnAlias; - - $sql .= $this->walkSimpleArithmeticExpression($expr) . ' AS ' . $columnAlias; - break; - case ($expr instanceof AST\NullIfExpression): case ($expr instanceof AST\CoalesceExpression): case ($expr instanceof AST\GeneralCaseExpression): case ($expr instanceof AST\SimpleCaseExpression): $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->_scalarResultCounter++; - - $columnAlias = 'sclr' . $this->_aliasCounter++; + + $columnAlias = $this->getSQLColumnAlias('sclr'); $this->_scalarResultAliasMap[$alias] = $columnAlias; - - $sql .= $this->walkCaseExpression($expr) . ' AS ' . $columnAlias; + + $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; break; - + default: // IdentificationVariable $class = $this->_queryComponents[$expr]['metadata']; $tableAlias = $this->getSQLTableAlias($class->getTableName(), $expr); $sqlParts = array(); - foreach ($class->identifier as $identifier) { - $sqlParts[] = $tableAlias . '.' . $class->getQuotedColumnName($identifier, $this->_platform); + foreach ($class->getQuotedIdentifierColumnNames($this->_platform) as $columnName) { + $sqlParts[] = $tableAlias . '.' . $columnName; } - + $sql .= implode(', ', $sqlParts); break; } - + return $sql; } @@ -1378,22 +1294,22 @@ class SqlWalker implements TreeWalker public function walkGroupByClause($groupByClause) { $sqlParts = array(); - + foreach ($groupByClause->groupByItems AS $groupByItem) { if ( ! is_string($groupByItem)) { $sqlParts[] = $this->walkGroupByItem($groupByItem); - + continue; } - + foreach ($this->_queryComponents[$groupByItem]['metadata']->identifier AS $idField) { $groupByItem = new AST\PathExpression(AST\PathExpression::TYPE_STATE_FIELD, $groupByItem, $idField); $groupByItem->type = AST\PathExpression::TYPE_STATE_FIELD; - + $sqlParts[] = $this->walkGroupByItem($groupByItem); } } - + return ' GROUP BY ' . implode(', ', $sqlParts); } @@ -1419,7 +1335,7 @@ class SqlWalker implements TreeWalker $class = $this->_em->getClassMetadata($deleteClause->abstractSchemaName); $tableName = $class->getTableName(); $sql = 'DELETE FROM ' . $class->getQuotedTableName($this->_platform); - + $this->setSQLTableAlias($tableName, $tableName, $deleteClause->aliasIdentificationVariable); $this->_rootAliases[] = $deleteClause->aliasIdentificationVariable; @@ -1437,7 +1353,7 @@ class SqlWalker implements TreeWalker $class = $this->_em->getClassMetadata($updateClause->abstractSchemaName); $tableName = $class->getTableName(); $sql = 'UPDATE ' . $class->getQuotedTableName($this->_platform); - + $this->setSQLTableAlias($tableName, $tableName, $updateClause->aliasIdentificationVariable); $this->_rootAliases[] = $updateClause->aliasIdentificationVariable; @@ -1464,11 +1380,11 @@ class SqlWalker implements TreeWalker case ($newValue instanceof AST\Node): $sql .= $newValue->dispatch($this); break; - + case ($newValue === null): $sql .= 'NULL'; break; - + default: $sql .= $this->_conn->quote($newValue); break; @@ -1493,8 +1409,8 @@ class SqlWalker implements TreeWalker if ($condSql) { return ' WHERE ' . (( ! $discrSql) ? $condSql : '(' . $condSql . ') AND ' . $discrSql); - } - + } + if ($discrSql) { return ' WHERE ' . $discrSql; } @@ -1515,7 +1431,7 @@ class SqlWalker implements TreeWalker if ( ! ($condExpr instanceof AST\ConditionalExpression)) { return $this->walkConditionalTerm($condExpr); } - + return implode(' OR ', array_map(array($this, 'walkConditionalTerm'), $condExpr->conditionalTerms)); } @@ -1532,7 +1448,7 @@ class SqlWalker implements TreeWalker if ( ! ($condTerm instanceof AST\ConditionalTerm)) { return $this->walkConditionalFactor($condTerm); } - + return implode(' AND ', array_map(array($this, 'walkConditionalFactor'), $condTerm->conditionalFactors)); } @@ -1561,8 +1477,8 @@ class SqlWalker implements TreeWalker { if ($primary->isSimpleConditionalExpression()) { return $primary->simpleConditionalExpression->dispatch($this); - } - + } + if ($primary->isConditionalExpression()) { $condExpr = $primary->conditionalExpression; @@ -1597,12 +1513,12 @@ class SqlWalker implements TreeWalker $sql .= 'EXISTS (SELECT 1 FROM '; $entityExpr = $collMemberExpr->entityExpression; $collPathExpr = $collMemberExpr->collectionValuedPathExpression; - + $fieldName = $collPathExpr->field; $dqlAlias = $collPathExpr->identificationVariable; - + $class = $this->_queryComponents[$dqlAlias]['metadata']; - + if ($entityExpr instanceof AST\InputParameter) { $dqlParamKey = $entityExpr->name; $entity = $this->_query->getParameter($dqlParamKey); @@ -1610,40 +1526,39 @@ class SqlWalker implements TreeWalker //TODO throw new \BadMethodCallException("Not implemented"); } - + $assoc = $class->associationMappings[$fieldName]; - + if ($assoc['type'] == ClassMetadata::ONE_TO_MANY) { $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName()); $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); - + $sql .= $targetClass->getQuotedTableName($this->_platform) . ' ' . $targetTableAlias . ' WHERE '; - + $owningAssoc = $targetClass->associationMappings[$assoc['mappedBy']]; $first = true; - + foreach ($owningAssoc['targetToSourceKeyColumns'] as $targetColumn => $sourceColumn) { if ($first) $first = false; else $sql .= ' AND '; - - $sql .= $sourceTableAlias . '.' . $class->getQuotedColumnName($class->fieldNames[$targetColumn], $this->_platform) - . ' = ' + + $sql .= $sourceTableAlias . '.' . $class->getQuotedColumnName($class->fieldNames[$targetColumn], $this->_platform) + . ' = ' . $targetTableAlias . '.' . $sourceColumn; } - + $sql .= ' AND '; $first = true; - - foreach ($targetClass->identifier as $idField) { + + foreach ($targetClass->getQuotedIdentifierColumnNames($this->_platform) as $targetColumnName) { if ($first) $first = false; else $sql .= ' AND '; - + $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); - $sql .= $targetTableAlias . '.' - . $targetClass->getQuotedColumnName($idField, $this->_platform) . ' = ?'; + $sql .= $targetTableAlias . '.' . $targetColumnName . ' = ?'; } } else { // many-to-many $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); - + $owningAssoc = $assoc['isOwningSide'] ? $assoc : $targetClass->associationMappings[$assoc['mappedBy']]; $joinTable = $owningAssoc['joinTable']; @@ -1651,11 +1566,11 @@ class SqlWalker implements TreeWalker $joinTableAlias = $this->getSQLTableAlias($joinTable['name']); $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName()); $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); - + // join to target table - $sql .= $targetClass->getQuotedJoinTableName($owningAssoc, $this->_platform) . ' ' . $joinTableAlias + $sql .= $targetClass->getQuotedJoinTableName($owningAssoc, $this->_platform) . ' ' . $joinTableAlias . ' INNER JOIN ' . $targetClass->getQuotedTableName($this->_platform) . ' ' . $targetTableAlias . ' ON '; - + // join conditions $joinColumns = $assoc['isOwningSide'] ? $joinTable['inverseJoinColumns'] @@ -1673,29 +1588,28 @@ class SqlWalker implements TreeWalker $joinColumns = $assoc['isOwningSide'] ? $joinTable['joinColumns'] : $joinTable['inverseJoinColumns']; $first = true; - + foreach ($joinColumns as $joinColumn) { if ($first) $first = false; else $sql .= ' AND '; - $sql .= $joinTableAlias . '.' . $joinColumn['name'] . ' = ' + $sql .= $joinTableAlias . '.' . $joinColumn['name'] . ' = ' . $sourceTableAlias . '.' . $class->getQuotedColumnName($class->fieldNames[$joinColumn['referencedColumnName']], $this->_platform); } - + $sql .= ' AND '; $first = true; - - foreach ($targetClass->identifier as $idField) { + + foreach ($targetClass->getQuotedIdentifierColumnNames($this->_platform) as $targetColumnName) { if ($first) $first = false; else $sql .= ' AND '; - + $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); - $sql .= $targetTableAlias . '.' - . $targetClass->getQuotedColumnName($idField, $this->_platform) . ' = ?'; + $sql .= $targetTableAlias . '.' . $targetColumnName . ' = ?'; } } return $sql . ')'; } - + /** * Walks down an EmptyCollectionComparisonExpression AST node, thereby generating the appropriate SQL. * @@ -1742,10 +1656,10 @@ class SqlWalker implements TreeWalker */ public function walkInExpression($inExpr) { - $sql = $this->walkPathExpression($inExpr->pathExpression) + $sql = $this->walkPathExpression($inExpr->pathExpression) . ($inExpr->not ? ' NOT' : '') . ' IN ('; - $sql .= ($inExpr->subselect) + $sql .= ($inExpr->subselect) ? $this->walkSubselect($inExpr->subselect) : implode(', ', array_map(array($this, 'walkInParameter'), $inExpr->literals)); @@ -1775,11 +1689,11 @@ class SqlWalker implements TreeWalker if ($this->_useSqlTableAliases) { $sql .= $this->getSQLTableAlias($discrClass->getTableName(), $dqlAlias) . '.'; } - + $sql .= $class->discriminatorColumn['name'] . ($instanceOfExpr->not ? ' NOT IN ' : ' IN '); - + $sqlParameterList = array(); - + foreach ($instanceOfExpr->value as $parameter) { if ($parameter instanceof AST\InputParameter) { // We need to modify the parameter value to be its correspondent mapped value @@ -1800,7 +1714,7 @@ class SqlWalker implements TreeWalker $sqlParameterList[] = $this->_conn->quote($class->discriminatorValue); } else { $discrMap = array_flip($class->discriminatorMap); - + if (!isset($discrMap[$entityClassName])) { throw QueryException::instanceOfUnrelatedClass($entityClassName, $class->rootEntityName); } @@ -1808,7 +1722,7 @@ class SqlWalker implements TreeWalker $sqlParameterList[] = $this->_conn->quote($discrMap[$entityClassName]); } } - + $sql .= '(' . implode(', ', $sqlParameterList) . ')'; return $sql; @@ -1822,8 +1736,8 @@ class SqlWalker implements TreeWalker */ public function walkInParameter($inParam) { - return $inParam instanceof AST\InputParameter - ? $this->walkInputParameter($inParam) + return $inParam instanceof AST\InputParameter + ? $this->walkInputParameter($inParam) : $this->walkLiteral($inParam); } @@ -1838,16 +1752,16 @@ class SqlWalker implements TreeWalker switch ($literal->type) { case AST\Literal::STRING: return $this->_conn->quote($literal->value); - + case AST\Literal::BOOLEAN: $bool = strtolower($literal->value) == 'true' ? true : false; $boolVal = $this->_conn->getDatabasePlatform()->convertBooleans($bool); - + return $boolVal; - + case AST\Literal::NUMERIC: return $literal->value; - + default: throw QueryException::invalidLiteral($literal); } @@ -1920,14 +1834,14 @@ class SqlWalker implements TreeWalker $leftExpr = $compExpr->leftExpression; $rightExpr = $compExpr->rightExpression; $sql = ''; - - $sql .= ($leftExpr instanceof AST\Node) + + $sql .= ($leftExpr instanceof AST\Node) ? $leftExpr->dispatch($this) : (is_numeric($leftExpr) ? $leftExpr : $this->_conn->quote($leftExpr)); $sql .= ' ' . $compExpr->operator . ' '; - $sql .= ($rightExpr instanceof AST\Node) + $sql .= ($rightExpr instanceof AST\Node) ? $rightExpr->dispatch($this) : (is_numeric($rightExpr) ? $rightExpr : $this->_conn->quote($rightExpr)); @@ -1971,7 +1885,7 @@ class SqlWalker implements TreeWalker if ( ! ($simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression)) { return $this->walkArithmeticTerm($simpleArithmeticExpr); } - + return implode(' ', array_map(array($this, 'walkArithmeticTerm'), $simpleArithmeticExpr->arithmeticTerms)); } @@ -1984,7 +1898,7 @@ class SqlWalker implements TreeWalker public function walkArithmeticTerm($term) { if (is_string($term)) { - return (isset($this->_queryComponents[$term])) + return (isset($this->_queryComponents[$term])) ? $this->_scalarResultAliasMap[$this->_queryComponents[$term]['token']['value']] : $term; } @@ -1994,7 +1908,7 @@ class SqlWalker implements TreeWalker if ( ! ($term instanceof AST\ArithmeticTerm)) { return $this->walkArithmeticFactor($term); } - + return implode(' ', array_map(array($this, 'walkArithmeticFactor'), $term->arithmeticFactors)); } @@ -2009,15 +1923,15 @@ class SqlWalker implements TreeWalker if (is_string($factor)) { return $factor; } - + // Phase 2 AST optimization: Skip processment of ArithmeticFactor // if only one ArithmeticPrimary is defined if ( ! ($factor instanceof AST\ArithmeticFactor)) { return $this->walkArithmeticPrimary($factor); } - + $sign = $factor->isNegativeSigned() ? '-' : ($factor->isPositiveSigned() ? '+' : ''); - + return $sign . $this->walkArithmeticPrimary($factor->arithmeticPrimary); } @@ -2031,8 +1945,8 @@ class SqlWalker implements TreeWalker { if ($primary instanceof AST\SimpleArithmeticExpression) { return '(' . $this->walkSimpleArithmeticExpression($primary) . ')'; - } - + } + if ($primary instanceof AST\Node) { return $primary->dispatch($this); } diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index 62efbac85..b1ba714d8 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -117,7 +117,7 @@ public function () * @param $ * @return */ -public function ($) +public function ($) { $this-> = $; return $this; @@ -406,7 +406,7 @@ public function () } if ($collections) { - return $this->_prefixCodeWithSpaces(str_replace("", implode("\n", $collections), self::$_constructorMethodTemplate)); + return $this->_prefixCodeWithSpaces(str_replace("", implode("\n".$this->_spaces, $collections), self::$_constructorMethodTemplate)); } return ''; @@ -634,7 +634,8 @@ public function () foreach ($metadata->associationMappings as $associationMapping) { if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) { - if ($code = $this->_generateEntityStubMethod($metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'])) { + $nullable = $this->_isAssociationIsNullable($associationMapping) ? 'null' : null; + if ($code = $this->_generateEntityStubMethod($metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'], $nullable)) { $methods[] = $code; } if ($code = $this->_generateEntityStubMethod($metadata, 'get', $associationMapping['fieldName'], $associationMapping['targetEntity'])) { @@ -653,6 +654,22 @@ public function () return implode("\n\n", $methods); } + private function _isAssociationIsNullable($associationMapping) + { + if (isset($associationMapping['joinColumns'])) { + $joinColumns = $associationMapping['joinColumns']; + } else { + //@todo thereis no way to retreive targetEntity metadata + $joinColumns = array(); + } + foreach ($joinColumns as $joinColumn) { + if(isset($joinColumn['nullable']) && !$joinColumn['nullable']) { + return false; + } + } + return true; + } + private function _generateEntityLifecycleCallbackMethods(ClassMetadataInfo $metadata) { if (isset($metadata->lifecycleCallbacks) && $metadata->lifecycleCallbacks) { @@ -707,7 +724,7 @@ public function () return implode("\n", $lines); } - private function _generateEntityStubMethod(ClassMetadataInfo $metadata, $type, $fieldName, $typeHint = null) + private function _generateEntityStubMethod(ClassMetadataInfo $metadata, $type, $fieldName, $typeHint = null, $defaultValue = null) { if ($type == "add") { $addMethod = explode("\\", $typeHint); @@ -737,6 +754,7 @@ public function () '' => Inflector::camelize($fieldName), '' => $methodName, '' => $fieldName, + '' => ($defaultValue !== null ) ? ('='.$defaultValue) : '', '' => $this->_getClassName($metadata) ); @@ -805,7 +823,12 @@ public function () { $lines = array(); $lines[] = $this->_spaces . '/**'; - $lines[] = $this->_spaces . ' * @var ' . $associationMapping['targetEntity']; + + if ($associationMapping['type'] & ClassMetadataInfo::TO_MANY) { + $lines[] = $this->_spaces . ' * @var \Doctrine\Common\Collections\ArrayCollection'; + } else { + $lines[] = $this->_spaces . ' * @var ' . $associationMapping['targetEntity']; + } if ($this->_generateAnnotations) { $lines[] = $this->_spaces . ' *'; diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index 77106ec7d..e052c69d7 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -67,7 +67,9 @@ class SchemaTool /** * Creates the database schema for the given array of ClassMetadata instances. * + * @throws ToolsException * @param array $classes + * @return void */ public function createSchema(array $classes) { @@ -75,7 +77,11 @@ class SchemaTool $conn = $this->_em->getConnection(); foreach ($createSchemaSql as $sql) { - $conn->executeQuery($sql); + try { + $conn->executeQuery($sql); + } catch(\Exception $e) { + throw ToolsException::schemaToolFailure($sql, $e); + } } } @@ -94,7 +100,7 @@ class SchemaTool /** * Some instances of ClassMetadata don't need to be processed in the SchemaTool context. This method detects them. - * + * * @param ClassMetadata $class * @param array $processedClasses * @return bool @@ -139,7 +145,7 @@ class SchemaTool $this->_gatherRelationsSql($class, $table, $schema); // Add the discriminator column - $discrColumnDef = $this->_getDiscriminatorColumnDefinition($class, $table); + $this->addDiscriminatorColumnDefinition($class, $table); // Aggregate all the information from all classes in the hierarchy foreach ($class->parentClasses as $parentClassName) { @@ -171,7 +177,7 @@ class SchemaTool // Add the discriminator column only to the root table if ($class->name == $class->rootEntityName) { - $discrColumnDef = $this->_getDiscriminatorColumnDefinition($class, $table); + $this->addDiscriminatorColumnDefinition($class, $table); } else { // Add an ID FK column to child tables /* @var Doctrine\ORM\Mapping\ClassMetadata $class */ @@ -261,7 +267,7 @@ class SchemaTool * @return array The portable column definition of the discriminator column as required by * the DBAL. */ - private function _getDiscriminatorColumnDefinition($class, $table) + private function addDiscriminatorColumnDefinition($class, $table) { $discrColumn = $class->discriminatorColumn; @@ -551,7 +557,7 @@ class SchemaTool try { $conn->executeQuery($sql); } catch(\Exception $e) { - + } } } @@ -589,7 +595,7 @@ class SchemaTool /** * Get SQL to drop the tables defined by the passed classes. - * + * * @param array $classes * @return array */ @@ -615,7 +621,7 @@ class SchemaTool } } } - + if ($this->_platform->supportsSequences()) { foreach ($schema->getSequences() AS $sequence) { $visitor->acceptSequence($sequence); @@ -659,7 +665,7 @@ 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, + * If $saveMode is set to true the command is executed in the Database, * else SQL is returned. * * @param array $classes The classes to consider. diff --git a/lib/Doctrine/ORM/Tools/SchemaValidator.php b/lib/Doctrine/ORM/Tools/SchemaValidator.php index a7f8e3a1c..cb3c9e515 100644 --- a/lib/Doctrine/ORM/Tools/SchemaValidator.php +++ b/lib/Doctrine/ORM/Tools/SchemaValidator.php @@ -50,7 +50,7 @@ class SchemaValidator } /** - * Checks the internal consistency of mapping files. + * Checks the internal consistency of all mapping files. * * There are several checks that can't be done at runtime or are too expensive, which can be verified * with this command. For example: @@ -69,150 +69,7 @@ class SchemaValidator $classes = $cmf->getAllMetadata(); foreach ($classes AS $class) { - $ce = array(); - /* @var $class ClassMetadata */ - foreach ($class->associationMappings AS $fieldName => $assoc) { - if (!$cmf->hasMetadataFor($assoc['targetEntity'])) { - $ce[] = "The target entity '" . $assoc['targetEntity'] . "' specified on " . $class->name . '#' . $fieldName . ' is unknown.'; - } - - if ($assoc['mappedBy'] && $assoc['inversedBy']) { - $ce[] = "The association " . $class . "#" . $fieldName . " cannot be defined as both inverse and owning."; - } - - $targetMetadata = $cmf->getMetadataFor($assoc['targetEntity']); - - /* @var $assoc AssociationMapping */ - if ($assoc['mappedBy']) { - if ($targetMetadata->hasField($assoc['mappedBy'])) { - $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". - "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which is not defined as association."; - } - if (!$targetMetadata->hasAssociation($assoc['mappedBy'])) { - $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". - "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which does not exist."; - } else if ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] == null) { - $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the inverse side of a ". - "bi-directional relationship, but the specified mappedBy association on the target-entity ". - $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ". - "'inversedBy' attribute."; - } else if ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] != $fieldName) { - $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " . - $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " are ". - "incosistent with each other."; - } - } - - if ($assoc['inversedBy']) { - if ($targetMetadata->hasField($assoc['inversedBy'])) { - $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ". - "field " . $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " which is not defined as association."; - } - if (!$targetMetadata->hasAssociation($assoc['inversedBy'])) { - $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ". - "field " . $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " which does not exist."; - } else if ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] == null) { - $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the owning side of a ". - "bi-directional relationship, but the specified mappedBy association on the target-entity ". - $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ". - "'inversedBy' attribute."; - } else if ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] != $fieldName) { - $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " . - $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " are ". - "incosistent with each other."; - } - } - - if ($assoc['isOwningSide']) { - if ($assoc['type'] == ClassMetadataInfo::MANY_TO_MANY) { - foreach ($assoc['joinTable']['joinColumns'] AS $joinColumn) { - if (!isset($class->fieldNames[$joinColumn['referencedColumnName']])) { - $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' does not " . - "have a corresponding field with this column name on the class '" . $class->name . "'."; - break; - } - - $fieldName = $class->fieldNames[$joinColumn['referencedColumnName']]; - if (!in_array($fieldName, $class->identifier)) { - $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " . - "has to be a primary key column."; - } - } - foreach ($assoc['joinTable']['inverseJoinColumns'] AS $inverseJoinColumn) { - $targetClass = $cmf->getMetadataFor($assoc['targetEntity']); - if (!isset($targetClass->fieldNames[$inverseJoinColumn['referencedColumnName']])) { - $ce[] = "The inverse referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' does not " . - "have a corresponding field with this column name on the class '" . $targetClass->name . "'."; - break; - } - - $fieldName = $targetClass->fieldNames[$inverseJoinColumn['referencedColumnName']]; - if (!in_array($fieldName, $targetClass->identifier)) { - $ce[] = "The referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' " . - "has to be a primary key column."; - } - } - - if (count($targetClass->identifier) != count($assoc['joinTable']['inverseJoinColumns'])) { - $ce[] = "The inverse join columns of the many-to-many table '" . $assoc['joinTable']['name'] . "' " . - "have to match to ALL identifier columns of the target entity '". $targetClass->name . "'"; - } - - if (count($class->identifier) != count($assoc['joinTable']['joinColumns'])) { - $ce[] = "The join columns of the many-to-many table '" . $assoc['joinTable']['name'] . "' " . - "have to match to ALL identifier columns of the source entity '". $class->name . "'"; - } - - } else if ($assoc['type'] & ClassMetadataInfo::TO_ONE) { - foreach ($assoc['joinColumns'] AS $joinColumn) { - $targetClass = $cmf->getMetadataFor($assoc['targetEntity']); - if (!isset($targetClass->fieldNames[$joinColumn['referencedColumnName']])) { - $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' does not " . - "have a corresponding field with this column name on the class '" . $targetClass->name . "'."; - break; - } - - $fieldName = $targetClass->fieldNames[$joinColumn['referencedColumnName']]; - if (!in_array($fieldName, $targetClass->identifier)) { - $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " . - "has to be a primary key column."; - } - } - - if (count($class->identifier) != count($assoc['joinColumns'])) { - $ce[] = "The join columns of the association '" . $assoc['fieldName'] . "' " . - "have to match to ALL identifier columns of the source entity '". $class->name . "'"; - } - } - } - - if (isset($assoc['orderBy']) && $assoc['orderBy'] !== null) { - $targetClass = $cmf->getMetadataFor($assoc['targetEntity']); - foreach ($assoc['orderBy'] AS $orderField => $orientation) { - if (!$targetClass->hasField($orderField)) { - $ce[] = "The association " . $class->name."#".$fieldName." is ordered by a foreign field " . - $orderField . " that is not a field on the target entity " . $targetClass->name; - } - } - } - } - - foreach ($class->reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $publicAttr) { - if ($publicAttr->isStatic()) { - continue; - } - $ce[] = "Field '".$publicAttr->getName()."' in class '".$class->name."' must be private ". - "or protected. Public fields may break lazy-loading."; - } - - foreach ($class->subClasses AS $subClass) { - if (!in_array($class->name, class_parents($subClass))) { - $ce[] = "According to the discriminator map class '" . $subClass . "' has to be a child ". - "of '" . $class->name . "' but these entities are not related through inheritance."; - } - } - - if ($ce) { + if ($ce = $this->validateClass($class)) { $errors[$class->name] = $ce; } } @@ -220,6 +77,170 @@ class SchemaValidator return $errors; } + /** + * Validate a single class of the current + * + * @param ClassMetadataInfo $class + * @return array + */ + public function validateClass(ClassMetadataInfo $class) + { + $ce = array(); + $cmf = $this->em->getMetadataFactory(); + + foreach ($class->associationMappings AS $fieldName => $assoc) { + if (!$cmf->hasMetadataFor($assoc['targetEntity'])) { + $ce[] = "The target entity '" . $assoc['targetEntity'] . "' specified on " . $class->name . '#' . $fieldName . ' is unknown.'; + return $ce; + } + + if ($assoc['mappedBy'] && $assoc['inversedBy']) { + $ce[] = "The association " . $class . "#" . $fieldName . " cannot be defined as both inverse and owning."; + } + + $targetMetadata = $cmf->getMetadataFor($assoc['targetEntity']); + + /* @var $assoc AssociationMapping */ + if ($assoc['mappedBy']) { + if ($targetMetadata->hasField($assoc['mappedBy'])) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". + "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which is not defined as association."; + } + if (!$targetMetadata->hasAssociation($assoc['mappedBy'])) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". + "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which does not exist."; + } else if ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] == null) { + $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the inverse side of a ". + "bi-directional relationship, but the specified mappedBy association on the target-entity ". + $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ". + "'inversedBy' attribute."; + } else if ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] != $fieldName) { + $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " . + $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " are ". + "incosistent with each other."; + } + } + + if ($assoc['inversedBy']) { + if ($targetMetadata->hasField($assoc['inversedBy'])) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ". + "field " . $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " which is not defined as association."; + } + if (!$targetMetadata->hasAssociation($assoc['inversedBy'])) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ". + "field " . $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " which does not exist."; + } else if ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] == null) { + $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the owning side of a ". + "bi-directional relationship, but the specified mappedBy association on the target-entity ". + $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ". + "'inversedBy' attribute."; + } else if ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] != $fieldName) { + $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " . + $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " are ". + "incosistent with each other."; + } + } + + if ($assoc['isOwningSide']) { + if ($assoc['type'] == ClassMetadataInfo::MANY_TO_MANY) { + foreach ($assoc['joinTable']['joinColumns'] AS $joinColumn) { + if (!isset($class->fieldNames[$joinColumn['referencedColumnName']])) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' does not " . + "have a corresponding field with this column name on the class '" . $class->name . "'."; + break; + } + + $fieldName = $class->fieldNames[$joinColumn['referencedColumnName']]; + if (!in_array($fieldName, $class->identifier)) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " . + "has to be a primary key column."; + } + } + foreach ($assoc['joinTable']['inverseJoinColumns'] AS $inverseJoinColumn) { + if (!isset($targetMetadata->fieldNames[$inverseJoinColumn['referencedColumnName']])) { + $ce[] = "The inverse referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' does not " . + "have a corresponding field with this column name on the class '" . $targetMetadata->name . "'."; + break; + } + + $fieldName = $targetMetadata->fieldNames[$inverseJoinColumn['referencedColumnName']]; + if (!in_array($fieldName, $targetMetadata->identifier)) { + $ce[] = "The referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' " . + "has to be a primary key column."; + } + } + + if (count($targetMetadata->getIdentifierColumnNames()) != count($assoc['joinTable']['inverseJoinColumns'])) { + $ce[] = "The inverse join columns of the many-to-many table '" . $assoc['joinTable']['name'] . "' " . + "have to contain to ALL identifier columns of the target entity '". $targetMetadata->name . "', " . + "however '" . implode(", ", array_diff($targetMetadata->getIdentifierColumnNames(), array_values($assoc['relationToTargetKeyColumns']))) . + "' are missing."; + } + + if (count($class->getIdentifierColumnNames()) != count($assoc['joinTable']['joinColumns'])) { + $ce[] = "The join columns of the many-to-many table '" . $assoc['joinTable']['name'] . "' " . + "have to contain to ALL identifier columns of the source entity '". $class->name . "', " . + "however '" . implode(", ", array_diff($class->getIdentifierColumnNames(), array_values($assoc['relationToSourceKeyColumns']))) . + "' are missing."; + } + + } else if ($assoc['type'] & ClassMetadataInfo::TO_ONE) { + foreach ($assoc['joinColumns'] AS $joinColumn) { + if (!isset($targetMetadata->fieldNames[$joinColumn['referencedColumnName']])) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' does not " . + "have a corresponding field with this column name on the class '" . $targetMetadata->name . "'."; + break; + } + + $fieldName = $targetMetadata->fieldNames[$joinColumn['referencedColumnName']]; + if (!in_array($fieldName, $targetMetadata->identifier)) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " . + "has to be a primary key column."; + } + } + + if (count($class->getIdentifierColumnNames()) != count($assoc['joinColumns'])) { + $ids = array(); + foreach ($assoc['joinColumns'] AS $joinColumn) { + $ids[] = $joinColumn['name']; + } + + $ce[] = "The join columns of the association '" . $assoc['fieldName'] . "' " . + "have to match to ALL identifier columns of the source entity '". $class->name . "', " . + "however '" . implode(", ", array_diff($class->getIdentifierColumnNames(), $ids)) . + "' are missing."; + } + } + } + + if (isset($assoc['orderBy']) && $assoc['orderBy'] !== null) { + foreach ($assoc['orderBy'] AS $orderField => $orientation) { + if (!$targetMetadata->hasField($orderField)) { + $ce[] = "The association " . $class->name."#".$fieldName." is ordered by a foreign field " . + $orderField . " that is not a field on the target entity " . $targetMetadata->name; + } + } + } + } + + foreach ($class->reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $publicAttr) { + if ($publicAttr->isStatic()) { + continue; + } + $ce[] = "Field '".$publicAttr->getName()."' in class '".$class->name."' must be private ". + "or protected. Public fields may break lazy-loading."; + } + + foreach ($class->subClasses AS $subClass) { + if (!in_array($class->name, class_parents($subClass))) { + $ce[] = "According to the discriminator map class '" . $subClass . "' has to be a child ". + "of '" . $class->name . "' but these entities are not related through inheritance."; + } + } + + return $ce; + } + /** * Check if the Database Schema is in sync with the current metadata state. * @@ -230,6 +251,6 @@ class SchemaValidator $schemaTool = new SchemaTool($this->em); $allMetadata = $this->em->getMetadataFactory()->getAllMetadata(); - return (count($schemaTool->getUpdateSchemaSql($allMetadata, false)) == 0); + return (count($schemaTool->getUpdateSchemaSql($allMetadata, true)) == 0); } } diff --git a/lib/Doctrine/ORM/Tools/ToolsException.php b/lib/Doctrine/ORM/Tools/ToolsException.php index f7ed87105..4551d87da 100644 --- a/lib/Doctrine/ORM/Tools/ToolsException.php +++ b/lib/Doctrine/ORM/Tools/ToolsException.php @@ -1,11 +1,38 @@ . + */ namespace Doctrine\ORM\Tools; use Doctrine\ORM\ORMException; +/** + * Tools related Exceptions + * + * @author Benjamin Eberlei + */ class ToolsException extends ORMException { + public static function schemaToolFailure($sql, \Exception $e) + { + return new self("Schema-Tool failed with Error '" . $e->getMessage() . "' while executing DDL: " . $sql, "0", $e); + } + public static function couldNotMapDoctrine1Type($type) { return new self("Could not map doctrine 1 type '$type'!"); diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index e499d3c5f..1404569c8 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -259,6 +259,11 @@ class UnitOfWork implements PropertyChangedListener */ public function commit($entity = null) { + // Raise preFlush + if ($this->evm->hasListeners(Events::preFlush)) { + $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->em)); + } + // Compute changes done since last commit. if ($entity === null) { $this->computeChangeSets(); @@ -290,8 +295,8 @@ class UnitOfWork implements PropertyChangedListener $commitOrder = $this->getCommitOrder(); $conn = $this->em->getConnection(); - $conn->beginTransaction(); + try { if ($this->entityInsertions) { foreach ($commitOrder as $class) { @@ -312,13 +317,11 @@ class UnitOfWork implements PropertyChangedListener // Collection deletions (deletions of complete collections) foreach ($this->collectionDeletions as $collectionToDelete) { - $this->getCollectionPersister($collectionToDelete->getMapping()) - ->delete($collectionToDelete); + $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete); } // Collection updates (deleteRows, updateRows, insertRows) foreach ($this->collectionUpdates as $collectionToUpdate) { - $this->getCollectionPersister($collectionToUpdate->getMapping()) - ->update($collectionToUpdate); + $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate); } // Entity deletions come last and need to be in reverse commit order @@ -332,6 +335,7 @@ class UnitOfWork implements PropertyChangedListener } catch (Exception $e) { $this->em->close(); $conn->rollback(); + throw $e; } @@ -367,6 +371,7 @@ class UnitOfWork implements PropertyChangedListener { foreach ($this->entityInsertions as $entity) { $class = $this->em->getClassMetadata(get_class($entity)); + $this->computeChangeSet($class, $entity); } } @@ -397,7 +402,7 @@ class UnitOfWork implements PropertyChangedListener // Compute changes for INSERTed entities first. This must always happen even in this case. $this->computeScheduleInsertsChangeSets(); - if ( $class->isReadOnly ) { + if ($class->isReadOnly) { return; } @@ -408,6 +413,7 @@ class UnitOfWork implements PropertyChangedListener // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here. $oid = spl_object_hash($entity); + if ( ! isset($this->entityInsertions[$oid]) && isset($this->entityStates[$oid])) { $this->computeChangeSet($class, $entity); } @@ -420,6 +426,7 @@ class UnitOfWork implements PropertyChangedListener { foreach ($this->extraUpdates as $oid => $update) { list ($entity, $changeset) = $update; + $this->entityChangeSets[$oid] = $changeset; $this->getEntityPersister(get_class($entity))->update($entity); } @@ -433,9 +440,11 @@ class UnitOfWork implements PropertyChangedListener public function getEntityChangeSet($entity) { $oid = spl_object_hash($entity); + if (isset($this->entityChangeSets[$oid])) { return $this->entityChangeSets[$oid]; } + return array(); } @@ -471,24 +480,27 @@ class UnitOfWork implements PropertyChangedListener */ public function computeChangeSet(ClassMetadata $class, $entity) { - if ( ! $class->isInheritanceTypeNone()) { - $class = $this->em->getClassMetadata(get_class($entity)); - } - $oid = spl_object_hash($entity); if (isset($this->readOnlyObjects[$oid])) { return; } + if ( ! $class->isInheritanceTypeNone()) { + $class = $this->em->getClassMetadata(get_class($entity)); + } + + // Fire PreFlush lifecycle callbacks + if (isset($class->lifecycleCallbacks[Events::preFlush])) { + $class->invokeLifecycleCallbacks(Events::preFlush, $entity); + } + $actualData = array(); + foreach ($class->reflFields as $name => $refProp) { $value = $refProp->getValue($entity); - if (isset($class->associationMappings[$name]) - && ($class->associationMappings[$name]['type'] & ClassMetadata::TO_MANY) - && $value !== null - && ! ($value instanceof PersistentCollection)) { - + + if ($class->isCollectionValuedAssociation($name) && $value !== null && ! ($value instanceof PersistentCollection)) { // If $value is not a Collection then use an ArrayCollection. if ( ! $value instanceof Collection) { $value = new ArrayCollection($value); @@ -497,17 +509,20 @@ class UnitOfWork implements PropertyChangedListener $assoc = $class->associationMappings[$name]; // Inject PersistentCollection - $coll = new PersistentCollection( - $this->em, - $this->em->getClassMetadata($assoc['targetEntity']), - $value + $value = new PersistentCollection( + $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value ); + $value->setOwner($entity, $assoc); + $value->setDirty( ! $value->isEmpty()); - $coll->setOwner($entity, $assoc); - $coll->setDirty( ! $coll->isEmpty()); - $class->reflFields[$name]->setValue($entity, $coll); - $actualData[$name] = $coll; - } else if ( (! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField) ) { + $class->reflFields[$name]->setValue($entity, $value); + + $actualData[$name] = $value; + + continue; + } + + if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) { $actualData[$name] = $value; } } @@ -517,68 +532,92 @@ class UnitOfWork implements PropertyChangedListener // These result in an INSERT. $this->originalEntityData[$oid] = $actualData; $changeSet = array(); + foreach ($actualData as $propName => $actualValue) { - if (isset($class->associationMappings[$propName])) { - $assoc = $class->associationMappings[$propName]; - if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { - $changeSet[$propName] = array(null, $actualValue); - } - } else { + if ( ! isset($class->associationMappings[$propName])) { + $changeSet[$propName] = array(null, $actualValue); + + continue; + } + + $assoc = $class->associationMappings[$propName]; + + if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { $changeSet[$propName] = array(null, $actualValue); } } + $this->entityChangeSets[$oid] = $changeSet; } else { // Entity is "fully" MANAGED: it was already fully persisted before // and we have a copy of the original data - $originalData = $this->originalEntityData[$oid]; + $originalData = $this->originalEntityData[$oid]; $isChangeTrackingNotify = $class->isChangeTrackingNotify(); - $changeSet = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid])) ? $this->entityChangeSets[$oid] : array(); + $changeSet = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid])) + ? $this->entityChangeSets[$oid] + : array(); foreach ($actualData as $propName => $actualValue) { - if (isset($originalData[$propName])) { - $orgValue = $originalData[$propName]; - } else if (array_key_exists($propName, $originalData)) { - $orgValue = null; - } else { - // skip field, its a partially omitted one! + // skip field, its a partially omitted one! + if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) { + continue; + } + + $orgValue = $originalData[$propName]; + + // skip if value havent changed + if ($orgValue === $actualValue) { + continue; + } + + // if regular field + if ( ! isset($class->associationMappings[$propName])) { + if ($isChangeTrackingNotify) { + continue; + } + + $changeSet[$propName] = array($orgValue, $actualValue); + + continue; + } + + $assoc = $class->associationMappings[$propName]; + + if ($orgValue instanceof PersistentCollection) { + // A PersistentCollection was de-referenced, so delete it. + $coid = spl_object_hash($orgValue); + + if (isset($this->collectionDeletions[$coid])) { + continue; + } + + $this->collectionDeletions[$coid] = $orgValue; + $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored. + continue; } - if (isset($class->associationMappings[$propName])) { - $assoc = $class->associationMappings[$propName]; - if ($assoc['type'] & ClassMetadata::TO_ONE && $orgValue !== $actualValue) { - if ($assoc['isOwningSide']) { - $changeSet[$propName] = array($orgValue, $actualValue); - } - if ($orgValue !== null && $assoc['orphanRemoval']) { - $this->scheduleOrphanRemoval($orgValue); - } - } else if ($orgValue instanceof PersistentCollection && $orgValue !== $actualValue) { - // A PersistentCollection was de-referenced, so delete it. - $coid = spl_object_hash($orgValue); - if ( ! isset($this->collectionDeletions[$coid]) ) { - $this->collectionDeletions[$coid] = $orgValue; - $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored. - } + if ($assoc['type'] & ClassMetadata::TO_ONE) { + if ($assoc['isOwningSide']) { + $changeSet[$propName] = array($orgValue, $actualValue); + } + + if ($orgValue !== null && $assoc['orphanRemoval']) { + $this->scheduleOrphanRemoval($orgValue); } - } else if ($isChangeTrackingNotify) { - continue; - } else if ($orgValue !== $actualValue) { - $changeSet[$propName] = array($orgValue, $actualValue); } } + if ($changeSet) { - $this->entityChangeSets[$oid] = $changeSet; + $this->entityChangeSets[$oid] = $changeSet; $this->originalEntityData[$oid] = $actualData; - $this->entityUpdates[$oid] = $entity; + $this->entityUpdates[$oid] = $entity; } } // Look for changes in associations of the entity foreach ($class->associationMappings as $field => $assoc) { - $val = $class->reflFields[$field]->getValue($entity); - if ($val !== null) { + if (($val = $class->reflFields[$field]->getValue($entity)) !== null) { $this->computeAssociationChanges($assoc, $val); } } @@ -605,18 +644,29 @@ class UnitOfWork implements PropertyChangedListener // If change tracking is explicit or happens through notification, then only compute // changes on entities of that type that are explicitly marked for synchronization. - $entitiesToProcess = ! $class->isChangeTrackingDeferredImplicit() ? - (isset($this->scheduledForDirtyCheck[$className]) ? - $this->scheduledForDirtyCheck[$className] : array()) - : $entities; + switch (true) { + case ($class->isChangeTrackingDeferredImplicit()): + $entitiesToProcess = $entities; + break; + + case (isset($this->scheduledForDirtyCheck[$className])): + $entitiesToProcess = $this->scheduledForDirtyCheck[$className]; + break; + + default: + $entitiesToProcess = array(); + + } foreach ($entitiesToProcess as $entity) { // Ignore uninitialized proxy objects if ($entity instanceof Proxy && ! $entity->__isInitialized__) { continue; } + // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here. $oid = spl_object_hash($entity); + if ( ! isset($this->entityInsertions[$oid]) && isset($this->entityStates[$oid])) { $this->computeChangeSet($class, $entity); } @@ -632,75 +682,96 @@ class UnitOfWork implements PropertyChangedListener */ private function computeAssociationChanges($assoc, $value) { + if ($value instanceof Proxy && ! $value->__isInitialized__) { + return; + } + if ($value instanceof PersistentCollection && $value->isDirty()) { $coid = spl_object_hash($value); + if ($assoc['isOwningSide']) { $this->collectionUpdates[$coid] = $value; } + $this->visitedCollections[$coid] = $value; } - - // Look through the entities, and in any of their associations, for transient (new) - // enities, recursively. ("Persistence by reachability") - if ($assoc['type'] & ClassMetadata::TO_ONE) { - if ($value instanceof Proxy && ! $value->__isInitialized__) { - return; // Ignore uninitialized proxy objects - } - $value = array($value); - } else if ($value instanceof PersistentCollection) { - // Unwrap. Uninitialized collections will simply be empty. - $value = $value->unwrap(); - } - - $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); - foreach ($value as $entry) { + + // Look through the entities, and in any of their associations, + // for transient (new) entities, recursively. ("Persistence by reachability") + // Unwrap. Uninitialized collections will simply be empty. + $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? array($value) : $value->unwrap(); + $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); + + foreach ($unwrappedValue as $key => $entry) { $state = $this->getEntityState($entry, self::STATE_NEW); - $oid = spl_object_hash($entry); - if ($state == self::STATE_NEW) { - if ( ! $assoc['isCascadePersist']) { - throw new InvalidArgumentException("A new entity was found through the relationship '" - . $assoc['sourceEntity'] . "#" . $assoc['fieldName'] . "' that was not" - . " configured to cascade persist operations for entity: " . self::objToStr($entry) . "." - . " Explicitly persist the new entity or configure cascading persist operations" - . " on the relationship. If you cannot find out which entity causes the problem" - . " implement '" . $assoc['targetEntity'] . "#__toString()' to get a clue."); - } - $this->persistNew($targetClass, $entry); - $this->computeChangeSet($targetClass, $entry); - } else if ($state == self::STATE_REMOVED) { - return new InvalidArgumentException("Removed entity detected during flush: " - . self::objToStr($entry).". Remove deleted entities from associations."); - } else if ($state == self::STATE_DETACHED) { - // Can actually not happen right now as we assume STATE_NEW, - // so the exception will be raised from the DBAL layer (constraint violation). - throw new InvalidArgumentException("A detached entity was found through a " - . "relationship during cascading a persist operation."); + $oid = spl_object_hash($entry); + + switch ($state) { + case self::STATE_NEW: + if ( ! $assoc['isCascadePersist']) { + $message = "A new entity was found through the relationship '%s#%s' that was not configured " . + ' to cascade persist operations for entity: %s. Explicitly persist the new entity or ' . + 'configure cascading persist operations on tbe relationship. If you cannot find out ' . + 'which entity causes the problem, implement %s#__toString() to get a clue.'; + + throw new InvalidArgumentException(sprintf( + $message, $assoc['sourceEntity'], $assoc['fieldName'], self::objToStr($entry), $assoc['targetEntity'] + )); + } + + $this->persistNew($targetClass, $entry); + $this->computeChangeSet($targetClass, $entry); + break; + + case self::STATE_REMOVED: + // Consume the $value as array (it's either an array or an ArrayAccess) + // and remove the element from Collection. + if ($assoc['type'] & ClassMetadata::TO_MANY) { + unset($value[$key]); + } + break; + + case self::STATE_DETACHED: + // Can actually not happen right now as we assume STATE_NEW, + // so the exception will be raised from the DBAL layer (constraint violation). + $message = 'A detached entity was found through a relationship during cascading a persist operation.'; + + throw new InvalidArgumentException($message); + break; + + default: + // MANAGED associated entities are already taken into account + // during changeset calculation anyway, since they are in the identity map. } - // MANAGED associated entities are already taken into account - // during changeset calculation anyway, since they are in the identity map. } } private function persistNew($class, $entity) { $oid = spl_object_hash($entity); + if (isset($class->lifecycleCallbacks[Events::prePersist])) { $class->invokeLifecycleCallbacks(Events::prePersist, $entity); } + if ($this->evm->hasListeners(Events::prePersist)) { $this->evm->dispatchEvent(Events::prePersist, new LifecycleEventArgs($entity, $this->em)); } $idGen = $class->idGenerator; + if ( ! $idGen->isPostInsertGenerator()) { $idValue = $idGen->generate($this->em, $entity); + if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) { - $this->entityIdentifiers[$oid] = array($class->identifier[0] => $idValue); - $class->setIdentifierValues($entity, $this->entityIdentifiers[$oid]); - } else { - $this->entityIdentifiers[$oid] = $idValue; + $idValue = array($class->identifier[0] => $idValue); + + $class->setIdentifierValues($entity, $idValue); } + + $this->entityIdentifiers[$oid] = $idValue; } + $this->entityStates[$oid] = self::STATE_MANAGED; $this->scheduleForInsert($entity); @@ -728,16 +799,17 @@ class UnitOfWork implements PropertyChangedListener throw new InvalidArgumentException('Entity must be managed.'); } - /* TODO: Just return if changetracking policy is NOTIFY? + // skip if change tracking is "NOTIFY" if ($class->isChangeTrackingNotify()) { return; - }*/ + } if ( ! $class->isInheritanceTypeNone()) { $class = $this->em->getClassMetadata(get_class($entity)); } $actualData = array(); + foreach ($class->reflFields as $name => $refProp) { if ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) { $actualData[$name] = $refProp->getValue($entity); @@ -749,6 +821,7 @@ class UnitOfWork implements PropertyChangedListener foreach ($actualData as $propName => $actualValue) { $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null; + if (is_object($orgValue) && $orgValue !== $actualValue) { $changeSet[$propName] = array($orgValue, $actualValue); } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) { @@ -760,6 +833,7 @@ class UnitOfWork implements PropertyChangedListener if (isset($this->entityChangeSets[$oid])) { $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet); } + $this->originalEntityData[$oid] = $actualData; } } @@ -773,20 +847,22 @@ class UnitOfWork implements PropertyChangedListener { $className = $class->name; $persister = $this->getEntityPersister($className); + $entities = array(); $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postPersist]); - $hasListeners = $this->evm->hasListeners(Events::postPersist); - if ($hasLifecycleCallbacks || $hasListeners) { - $entities = array(); - } + $hasListeners = $this->evm->hasListeners(Events::postPersist); foreach ($this->entityInsertions as $oid => $entity) { - if (get_class($entity) === $className) { - $persister->addInsert($entity); - unset($this->entityInsertions[$oid]); - if ($hasLifecycleCallbacks || $hasListeners) { - $entities[] = $entity; - } + if (get_class($entity) !== $className) { + continue; + } + + $persister->addInsert($entity); + + unset($this->entityInsertions[$oid]); + + if ($hasLifecycleCallbacks || $hasListeners) { + $entities[] = $entity; } } @@ -795,24 +871,26 @@ class UnitOfWork implements PropertyChangedListener if ($postInsertIds) { // Persister returned post-insert IDs foreach ($postInsertIds as $id => $entity) { - $oid = spl_object_hash($entity); + $oid = spl_object_hash($entity); $idField = $class->identifier[0]; + $class->reflFields[$idField]->setValue($entity, $id); + $this->entityIdentifiers[$oid] = array($idField => $id); $this->entityStates[$oid] = self::STATE_MANAGED; $this->originalEntityData[$oid][$idField] = $id; + $this->addToIdentityMap($entity); } } - if ($hasLifecycleCallbacks || $hasListeners) { - foreach ($entities as $entity) { - if ($hasLifecycleCallbacks) { - $class->invokeLifecycleCallbacks(Events::postPersist, $entity); - } - if ($hasListeners) { - $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($entity, $this->em)); - } + foreach ($entities as $entity) { + if ($hasLifecycleCallbacks) { + $class->invokeLifecycleCallbacks(Events::postPersist, $entity); + } + + if ($hasListeners) { + $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($entity, $this->em)); } } } @@ -828,35 +906,41 @@ class UnitOfWork implements PropertyChangedListener $persister = $this->getEntityPersister($className); $hasPreUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::preUpdate]); - $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate); + $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate); + $hasPostUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postUpdate]); - $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate); + $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate); foreach ($this->entityUpdates as $oid => $entity) { - if (get_class($entity) == $className || $entity instanceof Proxy && get_parent_class($entity) == $className) { + if ( ! (get_class($entity) === $className || $entity instanceof Proxy && get_parent_class($entity) === $className)) { + continue; + } - if ($hasPreUpdateLifecycleCallbacks) { - $class->invokeLifecycleCallbacks(Events::preUpdate, $entity); - $this->recomputeSingleEntityChangeSet($class, $entity); - } + if ($hasPreUpdateLifecycleCallbacks) { + $class->invokeLifecycleCallbacks(Events::preUpdate, $entity); - if ($hasPreUpdateListeners) { - $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs( - $entity, $this->em, $this->entityChangeSets[$oid]) - ); - } + $this->recomputeSingleEntityChangeSet($class, $entity); + } - if ($this->entityChangeSets[$oid]) { - $persister->update($entity); - } - unset($this->entityUpdates[$oid]); - - if ($hasPostUpdateLifecycleCallbacks) { - $class->invokeLifecycleCallbacks(Events::postUpdate, $entity); - } - if ($hasPostUpdateListeners) { - $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entity, $this->em)); - } + if ($hasPreUpdateListeners) { + $this->evm->dispatchEvent( + Events::preUpdate, + new Event\PreUpdateEventArgs($entity, $this->em, $this->entityChangeSets[$oid]) + ); + } + + if ($this->entityChangeSets[$oid]) { + $persister->update($entity); + } + + unset($this->entityUpdates[$oid]); + + if ($hasPostUpdateLifecycleCallbacks) { + $class->invokeLifecycleCallbacks(Events::postUpdate, $entity); + } + + if ($hasPostUpdateListeners) { + $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entity, $this->em)); } } } @@ -875,27 +959,32 @@ class UnitOfWork implements PropertyChangedListener $hasListeners = $this->evm->hasListeners(Events::postRemove); foreach ($this->entityDeletions as $oid => $entity) { - if (get_class($entity) == $className || $entity instanceof Proxy && get_parent_class($entity) == $className) { - $persister->delete($entity); - unset( - $this->entityDeletions[$oid], - $this->entityIdentifiers[$oid], - $this->originalEntityData[$oid], - $this->entityStates[$oid] - ); - // Entity with this $oid after deletion treated as NEW, even if the $oid - // is obtained by a new entity because the old one went out of scope. - //$this->entityStates[$oid] = self::STATE_NEW; - if ( ! $class->isIdentifierNatural()) { - $class->reflFields[$class->identifier[0]]->setValue($entity, null); - } + if ( ! (get_class($entity) == $className || $entity instanceof Proxy && get_parent_class($entity) == $className)) { + continue; + } + + $persister->delete($entity); + + unset( + $this->entityDeletions[$oid], + $this->entityIdentifiers[$oid], + $this->originalEntityData[$oid], + $this->entityStates[$oid] + ); + + // Entity with this $oid after deletion treated as NEW, even if the $oid + // is obtained by a new entity because the old one went out of scope. + //$this->entityStates[$oid] = self::STATE_NEW; + if ( ! $class->isIdentifierNatural()) { + $class->reflFields[$class->identifier[0]]->setValue($entity, null); + } - if ($hasLifecycleCallbacks) { - $class->invokeLifecycleCallbacks(Events::postRemove, $entity); - } - if ($hasListeners) { - $this->evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($entity, $this->em)); - } + if ($hasLifecycleCallbacks) { + $class->invokeLifecycleCallbacks(Events::postRemove, $entity); + } + + if ($hasListeners) { + $this->evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($entity, $this->em)); } } } @@ -908,51 +997,63 @@ class UnitOfWork implements PropertyChangedListener private function getCommitOrder(array $entityChangeSet = null) { if ($entityChangeSet === null) { - $entityChangeSet = array_merge( - $this->entityInsertions, - $this->entityUpdates, - $this->entityDeletions - ); + $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions); } $calc = $this->getCommitOrderCalculator(); // See if there are any new classes in the changeset, that are not in the // commit order graph yet (dont have a node). - - // TODO: Can we know the know the possible $newNodes based on something more efficient? IdentityMap? + // We have to inspect changeSet to be able to correctly build dependencies. + // It is not possible to use IdentityMap here because post inserted ids + // are not yet available. $newNodes = array(); + foreach ($entityChangeSet as $oid => $entity) { - $className = get_class($entity); - if ( ! $calc->hasClass($className)) { - $class = $this->em->getClassMetadata($className); - $calc->addClass($class); - $newNodes[] = $class; + $className = get_class($entity); + + if ($calc->hasClass($className)) { + continue; } + + $class = $this->em->getClassMetadata($className); + $calc->addClass($class); + + $newNodes[] = $class; } - + // Calculate dependencies for new nodes while ($class = array_pop($newNodes)) { foreach ($class->associationMappings as $assoc) { - if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { - $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); - if ( ! $calc->hasClass($targetClass->name)) { - $calc->addClass($targetClass); - $newNodes[] = $targetClass; - } - $calc->addDependency($targetClass, $class); - // If the target class has mapped subclasses, - // these share the same dependency. - if ($targetClass->subClasses) { - foreach ($targetClass->subClasses as $subClassName) { - $targetSubClass = $this->em->getClassMetadata($subClassName); - if ( ! $calc->hasClass($subClassName)) { - $calc->addClass($targetSubClass); - $newNodes[] = $targetSubClass; - } - $calc->addDependency($targetSubClass, $class); - } + if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) { + continue; + } + + $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); + + if ( ! $calc->hasClass($targetClass->name)) { + $calc->addClass($targetClass); + + $newNodes[] = $targetClass; + } + + $calc->addDependency($targetClass, $class); + + // If the target class has mapped subclasses, these share the same dependency. + if ( ! $targetClass->subClasses) { + continue; + } + + foreach ($targetClass->subClasses as $subClassName) { + $targetSubClass = $this->em->getClassMetadata($subClassName); + + if ( ! $calc->hasClass($subClassName)) { + $calc->addClass($targetSubClass); + + $newNodes[] = $targetSubClass; } + + $calc->addDependency($targetSubClass, $class); } } } @@ -973,9 +1074,11 @@ class UnitOfWork implements PropertyChangedListener if (isset($this->entityUpdates[$oid])) { throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion."); } + if (isset($this->entityDeletions[$oid])) { throw new InvalidArgumentException("Removed entity can not be scheduled for insertion."); } + if (isset($this->entityInsertions[$oid])) { throw new InvalidArgumentException("Entity can not be scheduled for insertion twice."); } @@ -1006,9 +1109,11 @@ class UnitOfWork implements PropertyChangedListener public function scheduleForUpdate($entity) { $oid = spl_object_hash($entity); + if ( ! isset($this->entityIdentifiers[$oid])) { throw new InvalidArgumentException("Entity has no identity."); } + if (isset($this->entityDeletions[$oid])) { throw new InvalidArgumentException("Entity is removed."); } @@ -1031,13 +1136,16 @@ class UnitOfWork implements PropertyChangedListener */ public function scheduleExtraUpdate($entity, array $changeset) { - $oid = spl_object_hash($entity); + $oid = spl_object_hash($entity); + $extraUpdate = array($entity, $changeset); + if (isset($this->extraUpdates[$oid])) { list($ignored, $changeset2) = $this->extraUpdates[$oid]; - $this->extraUpdates[$oid] = array($entity, $changeset + $changeset2); - } else { - $this->extraUpdates[$oid] = array($entity, $changeset); + + $extraUpdate = array($entity, $changeset + $changeset2); } + + $this->extraUpdates[$oid] = $extraUpdate; } /** @@ -1053,9 +1161,17 @@ class UnitOfWork implements PropertyChangedListener return isset($this->entityUpdates[spl_object_hash($entity)]); } + + /** + * Checks whether an entity is registered to be checked in the unit of work. + * + * @param object $entity + * @return boolean + */ public function isScheduledForDirtyCheck($entity) { $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName; + return isset($this->scheduledForDirtyCheck[$rootEntityName][spl_object_hash($entity)]); } @@ -1073,22 +1189,25 @@ class UnitOfWork implements PropertyChangedListener if ($this->isInIdentityMap($entity)) { $this->removeFromIdentityMap($entity); } + unset($this->entityInsertions[$oid], $this->entityStates[$oid]); + return; // entity has not been persisted yet, so nothing more to do. } if ( ! $this->isInIdentityMap($entity)) { - return; // ignore + return; } - + $this->removeFromIdentityMap($entity); - + if (isset($this->entityUpdates[$oid])) { unset($this->entityUpdates[$oid]); } + if ( ! isset($this->entityDeletions[$oid])) { $this->entityDeletions[$oid] = $entity; - $this->entityStates[$oid] = self::STATE_REMOVED; + $this->entityStates[$oid] = self::STATE_REMOVED; } } @@ -1113,9 +1232,10 @@ class UnitOfWork implements PropertyChangedListener public function isEntityScheduled($entity) { $oid = spl_object_hash($entity); - return isset($this->entityInsertions[$oid]) || - isset($this->entityUpdates[$oid]) || - isset($this->entityDeletions[$oid]); + + return isset($this->entityInsertions[$oid]) + || isset($this->entityUpdates[$oid]) + || isset($this->entityDeletions[$oid]); } /** @@ -1132,18 +1252,24 @@ class UnitOfWork implements PropertyChangedListener public function addToIdentityMap($entity) { $classMetadata = $this->em->getClassMetadata(get_class($entity)); - $idHash = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]); + $idHash = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]); + if ($idHash === '') { - throw new InvalidArgumentException("The given entity has no identity."); + throw new InvalidArgumentException('The given entity has no identity.'); } + $className = $classMetadata->rootEntityName; + if (isset($this->identityMap[$className][$idHash])) { return false; } + $this->identityMap[$className][$idHash] = $entity; + if ($entity instanceof NotifyPropertyChanged) { $entity->addPropertyChangedListener($this); } + return true; } @@ -1160,61 +1286,67 @@ class UnitOfWork implements PropertyChangedListener public function getEntityState($entity, $assume = null) { $oid = spl_object_hash($entity); - if ( ! isset($this->entityStates[$oid])) { - // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known. - // Note that you can not remember the NEW or DETACHED state in _entityStates since - // the UoW does not hold references to such objects and the object hash can be reused. - // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it. - if ($assume === null) { - $class = $this->em->getClassMetadata(get_class($entity)); - $id = $class->getIdentifierValues($entity); - if ( ! $id) { - return self::STATE_NEW; - } else if ($class->isIdentifierNatural()) { - // Check for a version field, if available, to avoid a db lookup. - if ($class->isVersioned) { - if ($class->getFieldValue($entity, $class->versionField)) { - return self::STATE_DETACHED; - } else { - return self::STATE_NEW; - } - } else { - // Last try before db lookup: check the identity map. - if ($this->tryGetById($id, $class->rootEntityName)) { - return self::STATE_DETACHED; - } else { - // db lookup - if ($this->getEntityPersister(get_class($entity))->exists($entity)) { - return self::STATE_DETACHED; - } else { - return self::STATE_NEW; - } - } - } - } else if (!$class->idGenerator->isPostInsertGenerator()) { - // if we have a pre insert generator we can't be sure that having an id - // really means that the entity exists. We have to verify this through - // the last resort: a db lookup - - // Last try before db lookup: check the identity map. - if ($this->tryGetById($id, $class->rootEntityName)) { - return self::STATE_DETACHED; - } else { - // db lookup - if ($this->getEntityPersister(get_class($entity))->exists($entity)) { - return self::STATE_DETACHED; - } else { - return self::STATE_NEW; - } - } - } else { + + if (isset($this->entityStates[$oid])) { + return $this->entityStates[$oid]; + } + + if ($assume !== null) { + return $assume; + } + + // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known. + // Note that you can not remember the NEW or DETACHED state in _entityStates since + // the UoW does not hold references to such objects and the object hash can be reused. + // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it. + $class = $this->em->getClassMetadata(get_class($entity)); + $id = $class->getIdentifierValues($entity); + + if ( ! $id) { + return self::STATE_NEW; + } + + switch (true) { + case ($class->isIdentifierNatural()); + // Check for a version field, if available, to avoid a db lookup. + if ($class->isVersioned) { + return ($class->getFieldValue($entity, $class->versionField)) + ? self::STATE_DETACHED + : self::STATE_NEW; + } + + // Last try before db lookup: check the identity map. + if ($this->tryGetById($id, $class->rootEntityName)) { return self::STATE_DETACHED; } - } else { - return $assume; - } + + // db lookup + if ($this->getEntityPersister(get_class($entity))->exists($entity)) { + return self::STATE_DETACHED; + } + + return self::STATE_NEW; + + case ( ! $class->idGenerator->isPostInsertGenerator()): + // if we have a pre insert generator we can't be sure that having an id + // really means that the entity exists. We have to verify this through + // the last resort: a db lookup + + // Last try before db lookup: check the identity map. + if ($this->tryGetById($id, $class->rootEntityName)) { + return self::STATE_DETACHED; + } + + // db lookup + if ($this->getEntityPersister(get_class($entity))->exists($entity)) { + return self::STATE_DETACHED; + } + + return self::STATE_NEW; + + default: + return self::STATE_DETACHED; } - return $this->entityStates[$oid]; } /** @@ -1228,16 +1360,21 @@ class UnitOfWork implements PropertyChangedListener */ public function removeFromIdentityMap($entity) { - $oid = spl_object_hash($entity); + $oid = spl_object_hash($entity); $classMetadata = $this->em->getClassMetadata(get_class($entity)); - $idHash = implode(' ', $this->entityIdentifiers[$oid]); + $idHash = implode(' ', $this->entityIdentifiers[$oid]); + if ($idHash === '') { - throw new InvalidArgumentException("The given entity has no identity."); + throw new InvalidArgumentException('The given entity has no identity.'); } + $className = $classMetadata->rootEntityName; + if (isset($this->identityMap[$className][$idHash])) { unset($this->identityMap[$className][$idHash]); + //$this->entityStates[$oid] = self::STATE_DETACHED; + return true; } @@ -1270,8 +1407,11 @@ class UnitOfWork implements PropertyChangedListener */ public function tryGetByIdHash($idHash, $rootClassName) { - return isset($this->identityMap[$rootClassName][$idHash]) ? - $this->identityMap[$rootClassName][$idHash] : false; + if (isset($this->identityMap[$rootClassName][$idHash])) { + return $this->identityMap[$rootClassName][$idHash]; + } + + return false; } /** @@ -1283,11 +1423,14 @@ class UnitOfWork implements PropertyChangedListener public function isInIdentityMap($entity) { $oid = spl_object_hash($entity); + if ( ! isset($this->entityIdentifiers[$oid])) { return false; } + $classMetadata = $this->em->getClassMetadata(get_class($entity)); - $idHash = implode(' ', $this->entityIdentifiers[$oid]); + $idHash = implode(' ', $this->entityIdentifiers[$oid]); + if ($idHash === '') { return false; } @@ -1317,6 +1460,7 @@ class UnitOfWork implements PropertyChangedListener public function persist($entity) { $visited = array(); + $this->doPersist($entity, $visited); } @@ -1332,6 +1476,7 @@ class UnitOfWork implements PropertyChangedListener private function doPersist($entity, array &$visited) { $oid = spl_object_hash($entity); + if (isset($visited[$oid])) { return; // Prevent infinite recursion } @@ -1353,19 +1498,24 @@ class UnitOfWork implements PropertyChangedListener $this->scheduleForDirtyCheck($entity); } break; + case self::STATE_NEW: $this->persistNew($class, $entity); break; + case self::STATE_REMOVED: // Entity becomes managed again unset($this->entityDeletions[$oid]); + $this->entityStates[$oid] = self::STATE_MANAGED; break; + case self::STATE_DETACHED: // Can actually not happen right now since we assume STATE_NEW. - throw new InvalidArgumentException("Detached entity passed to persist()."); + throw new InvalidArgumentException('Detached entity passed to persist().'); + default: - throw new UnexpectedValueException("Unexpected entity state: $entityState."); + throw new UnexpectedValueException(sprintf('Unexpected entity state: %s', $entityState)); } $this->cascadePersist($entity, $visited); @@ -1379,6 +1529,7 @@ class UnitOfWork implements PropertyChangedListener public function remove($entity) { $visited = array(); + $this->doRemove($entity, $visited); } @@ -1395,6 +1546,7 @@ class UnitOfWork implements PropertyChangedListener private function doRemove($entity, array &$visited) { $oid = spl_object_hash($entity); + if (isset($visited[$oid])) { return; // Prevent infinite recursion } @@ -1405,26 +1557,32 @@ class UnitOfWork implements PropertyChangedListener // can cause problems when a lazy proxy has to be initialized for the cascade operation. $this->cascadeRemove($entity, $visited); - $class = $this->em->getClassMetadata(get_class($entity)); + $class = $this->em->getClassMetadata(get_class($entity)); $entityState = $this->getEntityState($entity); + switch ($entityState) { case self::STATE_NEW: case self::STATE_REMOVED: // nothing to do break; + case self::STATE_MANAGED: if (isset($class->lifecycleCallbacks[Events::preRemove])) { $class->invokeLifecycleCallbacks(Events::preRemove, $entity); } + if ($this->evm->hasListeners(Events::preRemove)) { $this->evm->dispatchEvent(Events::preRemove, new LifecycleEventArgs($entity, $this->em)); } + $this->scheduleForDelete($entity); break; + case self::STATE_DETACHED: - throw new InvalidArgumentException("A detached entity can not be removed."); + throw new InvalidArgumentException('A detached entity can not be removed.'); + default: - throw new UnexpectedValueException("Unexpected entity state: $entityState."); + throw new UnexpectedValueException(sprintf('Unexpected entity state: %s', $entityState)); } } @@ -1442,6 +1600,7 @@ class UnitOfWork implements PropertyChangedListener public function merge($entity) { $visited = array(); + return $this->doMerge($entity, $visited); } @@ -1458,6 +1617,7 @@ class UnitOfWork implements PropertyChangedListener private function doMerge($entity, array &$visited, $prevManagedCopy = null, $assoc = null) { $oid = spl_object_hash($entity); + if (isset($visited[$oid])) { return; // Prevent infinite recursion } @@ -1470,9 +1630,9 @@ class UnitOfWork implements PropertyChangedListener // an extra db-roundtrip this way. If it is not MANAGED but has an identity, // we need to fetch it from the db anyway in order to merge. // MANAGED entities are ignored by the merge operation. - if ($this->getEntityState($entity, self::STATE_DETACHED) == self::STATE_MANAGED) { - $managedCopy = $entity; - } else { + $managedCopy = $entity; + + if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) { if ($entity instanceof Proxy && ! $entity->__isInitialized__) { $entity->__load(); } @@ -1483,9 +1643,11 @@ class UnitOfWork implements PropertyChangedListener // If there is no ID, it is actually NEW. if ( ! $id) { $managedCopy = $class->newInstance(); + $this->persistNew($class, $managedCopy); } else { $managedCopy = $this->tryGetById($id, $class->rootEntityName); + if ($managedCopy) { // We have the entity in-memory already, just make sure its not removed. if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) { @@ -1500,19 +1662,21 @@ class UnitOfWork implements PropertyChangedListener if ($managedCopy === null) { // If the identifier is ASSIGNED, it is NEW, otherwise an error // since the managed entity was not found. - if ($class->isIdentifierNatural()) { - $managedCopy = $class->newInstance(); - $class->setIdentifierValues($managedCopy, $id); - $this->persistNew($class, $managedCopy); - } else { + if ( ! $class->isIdentifierNatural()) { throw new EntityNotFoundException; } + + $managedCopy = $class->newInstance(); + $class->setIdentifierValues($managedCopy, $id); + + $this->persistNew($class, $managedCopy); } } if ($class->isVersioned) { $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy); $entityVersion = $class->reflFields[$class->versionField]->getValue($entity); + // Throw exception if versions dont match. if ($managedCopyVersion != $entityVersion) { throw OptimisticLockException::lockFailedVersionMissmatch($entity, $entityVersion, $managedCopyVersion); @@ -1571,10 +1735,12 @@ class UnitOfWork implements PropertyChangedListener } if ($assoc2['isCascadeMerge']) { $managedCol->initialize(); + // clear and set dirty a managed collection if its not also the same collection to merge from. if (!$managedCol->isEmpty() && $managedCol != $mergeCol) { $managedCol->unwrap()->clear(); $managedCol->setDirty(true); + if ($assoc2['isOwningSide'] && $assoc2['type'] == ClassMetadata::MANY_TO_MANY && $class->isChangeTrackingNotify()) { $this->scheduleForDirtyCheck($managedCopy); } @@ -1582,11 +1748,13 @@ class UnitOfWork implements PropertyChangedListener } } } + if ($class->isChangeTrackingNotify()) { // Just treat all properties as changed, there is no other choice. $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy)); } } + if ($class->isChangeTrackingDeferredExplicit()) { $this->scheduleForDirtyCheck($entity); } @@ -1595,10 +1763,12 @@ class UnitOfWork implements PropertyChangedListener if ($prevManagedCopy !== null) { $assocField = $assoc['fieldName']; $prevClass = $this->em->getClassMetadata(get_class($prevManagedCopy)); + if ($assoc['type'] & ClassMetadata::TO_ONE) { $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy); } else { $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy); + if ($assoc['type'] == ClassMetadata::ONE_TO_MANY) { $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy); } @@ -1830,6 +2000,7 @@ class UnitOfWork implements PropertyChangedListener private function cascadeRemove($entity, array &$visited) { $class = $this->em->getClassMetadata(get_class($entity)); + foreach ($class->associationMappings as $assoc) { if ( ! $assoc['isCascadeRemove']) { continue; @@ -1840,6 +2011,7 @@ class UnitOfWork implements PropertyChangedListener } $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity); + if ($relatedEntities instanceof Collection || is_array($relatedEntities)) { // If its a PersistentCollection initialization is intended! No unwrap! foreach ($relatedEntities as $relatedEntity) { @@ -1927,6 +2099,7 @@ class UnitOfWork implements PropertyChangedListener $this->collectionUpdates = $this->extraUpdates = $this->orphanRemovals = array(); + if ($this->commitOrderCalculator !== null) { $this->commitOrderCalculator->clear(); } @@ -1968,14 +2141,20 @@ class UnitOfWork implements PropertyChangedListener */ public function scheduleCollectionDeletion(PersistentCollection $coll) { + $coid = spl_object_hash($coll); + //TODO: if $coll is already scheduled for recreation ... what to do? // Just remove $coll from the scheduled recreations? - $this->collectionDeletions[spl_object_hash($coll)] = $coll; + if (isset($this->collectionUpdates[$coid])) { + unset($this->collectionUpdates[$coid]); + } + + $this->collectionDeletions[$coid] = $coll; } public function isCollectionScheduledForDeletion(PersistentCollection $coll) { - return isset( $this->collectionsDeletions[spl_object_hash($coll)] ); + return isset($this->collectionsDeletions[spl_object_hash($coll)]); } /** @@ -2026,6 +2205,11 @@ class UnitOfWork implements PropertyChangedListener } } else { $overrideLocalValues = isset($hints[Query::HINT_REFRESH]); + + // If only a specific entity is set to refresh, check that it's the one + if(isset($hints[Query::HINT_REFRESH_ENTITY])) { + $overrideLocalValues = $hints[Query::HINT_REFRESH_ENTITY] === $entity; + } } if ($overrideLocalValues) { @@ -2179,6 +2363,7 @@ class UnitOfWork implements PropertyChangedListener if (isset($class->lifecycleCallbacks[Events::postLoad])) { $class->invokeLifecycleCallbacks(Events::postLoad, $entity); } + if ($this->evm->hasListeners(Events::postLoad)) { $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity, $this->em)); } @@ -2191,17 +2376,20 @@ class UnitOfWork implements PropertyChangedListener */ public function triggerEagerLoads() { - if (!$this->eagerLoadingEntities) { + if ( ! $this->eagerLoadingEntities) { return; } // avoid infinite recursion - $eagerLoadingEntities = $this->eagerLoadingEntities; + $eagerLoadingEntities = $this->eagerLoadingEntities; $this->eagerLoadingEntities = array(); - foreach ($eagerLoadingEntities AS $entityName => $ids) { + foreach ($eagerLoadingEntities as $entityName => $ids) { $class = $this->em->getClassMetadata($entityName); - $this->getEntityPersister($entityName)->loadAll(array_combine($class->identifier, array(array_values($ids)))); + + $this->getEntityPersister($entityName)->loadAll( + array_combine($class->identifier, array(array_values($ids))) + ); } } @@ -2213,15 +2401,16 @@ class UnitOfWork implements PropertyChangedListener */ public function loadCollection(PersistentCollection $collection) { - $assoc = $collection->getMapping(); + $assoc = $collection->getMapping(); + $persister = $this->getEntityPersister($assoc['targetEntity']); + switch ($assoc['type']) { case ClassMetadata::ONE_TO_MANY: - $this->getEntityPersister($assoc['targetEntity'])->loadOneToManyCollection( - $assoc, $collection->getOwner(), $collection); + $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection); break; + case ClassMetadata::MANY_TO_MANY: - $this->getEntityPersister($assoc['targetEntity'])->loadManyToManyCollection( - $assoc, $collection->getOwner(), $collection); + $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection); break; } } @@ -2246,9 +2435,11 @@ class UnitOfWork implements PropertyChangedListener public function getOriginalEntityData($entity) { $oid = spl_object_hash($entity); + if (isset($this->originalEntityData[$oid])) { return $this->originalEntityData[$oid]; } + return array(); } @@ -2317,6 +2508,7 @@ class UnitOfWork implements PropertyChangedListener public function scheduleForDirtyCheck($entity) { $rootClassName = $this->em->getClassMetadata(get_class($entity))->rootEntityName; + $this->scheduledForDirtyCheck[$rootClassName][spl_object_hash($entity)] = $entity; } @@ -2338,34 +2530,45 @@ class UnitOfWork implements PropertyChangedListener */ public function size() { - $count = 0; - foreach ($this->identityMap as $entitySet) { - $count += count($entitySet); - } - return $count; + $countArray = array_map(function ($item) { return count($item); }, $this->identityMap); + + return array_sum($countArray); } /** * Gets the EntityPersister for an Entity. * * @param string $entityName The name of the Entity. + * * @return Doctrine\ORM\Persisters\AbstractEntityPersister */ public function getEntityPersister($entityName) { - if ( ! isset($this->persisters[$entityName])) { - $class = $this->em->getClassMetadata($entityName); - if ($class->isInheritanceTypeNone()) { - $persister = new Persisters\BasicEntityPersister($this->em, $class); - } else if ($class->isInheritanceTypeSingleTable()) { - $persister = new Persisters\SingleTablePersister($this->em, $class); - } else if ($class->isInheritanceTypeJoined()) { - $persister = new Persisters\JoinedSubclassPersister($this->em, $class); - } else { - $persister = new Persisters\UnionSubclassPersister($this->em, $class); - } - $this->persisters[$entityName] = $persister; + if (isset($this->persisters[$entityName])) { + return $this->persisters[$entityName]; } + + $class = $this->em->getClassMetadata($entityName); + + switch (true) { + case ($class->isInheritanceTypeNone()): + $persister = new Persisters\BasicEntityPersister($this->em, $class); + break; + + case ($class->isInheritanceTypeSingleTable()): + $persister = new Persisters\SingleTablePersister($this->em, $class); + break; + + case ($class->isInheritanceTypeJoined()): + $persister = new Persisters\JoinedSubclassPersister($this->em, $class); + break; + + default: + $persister = new Persisters\UnionSubclassPersister($this->em, $class); + } + + $this->persisters[$entityName] = $persister; + return $this->persisters[$entityName]; } @@ -2373,19 +2576,29 @@ class UnitOfWork implements PropertyChangedListener * Gets a collection persister for a collection-valued association. * * @param AssociationMapping $association + * * @return AbstractCollectionPersister */ public function getCollectionPersister(array $association) { $type = $association['type']; - if ( ! isset($this->collectionPersisters[$type])) { - if ($type == ClassMetadata::ONE_TO_MANY) { - $persister = new Persisters\OneToManyPersister($this->em); - } else if ($type == ClassMetadata::MANY_TO_MANY) { - $persister = new Persisters\ManyToManyPersister($this->em); - } - $this->collectionPersisters[$type] = $persister; + + if (isset($this->collectionPersisters[$type])) { + return $this->collectionPersisters[$type]; } + + switch ($type) { + case ClassMetadata::ONE_TO_MANY: + $persister = new Persisters\OneToManyPersister($this->em); + break; + + case ClassMetadata::MANY_TO_MANY: + $persister = new Persisters\ManyToManyPersister($this->em); + break; + } + + $this->collectionPersisters[$type] = $persister; + return $this->collectionPersisters[$type]; } @@ -2400,9 +2613,11 @@ class UnitOfWork implements PropertyChangedListener public function registerManaged($entity, array $id, array $data) { $oid = spl_object_hash($entity); - $this->entityIdentifiers[$oid] = $id; - $this->entityStates[$oid] = self::STATE_MANAGED; + + $this->entityIdentifiers[$oid] = $id; + $this->entityStates[$oid] = self::STATE_MANAGED; $this->originalEntityData[$oid] = $data; + $this->addToIdentityMap($entity); } @@ -2429,7 +2644,7 @@ class UnitOfWork implements PropertyChangedListener */ public function propertyChanged($entity, $propertyName, $oldValue, $newValue) { - $oid = spl_object_hash($entity); + $oid = spl_object_hash($entity); $class = $this->em->getClassMetadata(get_class($entity)); $isAssocField = isset($class->associationMappings[$propertyName]); @@ -2440,6 +2655,7 @@ class UnitOfWork implements PropertyChangedListener // Update changeset and mark entity for synchronization $this->entityChangeSets[$oid][$propertyName] = array($oldValue, $newValue); + if ( ! isset($this->scheduledForDirtyCheck[$class->rootEntityName][$oid])) { $this->scheduleForDirtyCheck($entity); } @@ -2505,7 +2721,11 @@ class UnitOfWork implements PropertyChangedListener { if ($obj instanceof Proxy) { $obj->__load(); - } else if ($obj instanceof PersistentCollection) { + + return; + } + + if ($obj instanceof PersistentCollection) { $obj->initialize(); } } @@ -2536,6 +2756,7 @@ class UnitOfWork implements PropertyChangedListener if ( ! is_object($object) || ! $this->isInIdentityMap($object)) { throw new InvalidArgumentException("Managed entity required"); } + $this->readOnlyObjects[spl_object_hash($object)] = true; } @@ -2551,6 +2772,7 @@ class UnitOfWork implements PropertyChangedListener if ( ! is_object($object) ) { throw new InvalidArgumentException("Managed entity required"); } + return isset($this->readOnlyObjects[spl_object_hash($object)]); } } diff --git a/tests/Doctrine/Tests/Mocks/ConnectionMock.php b/tests/Doctrine/Tests/Mocks/ConnectionMock.php index fabecf87a..c1c84d174 100644 --- a/tests/Doctrine/Tests/Mocks/ConnectionMock.php +++ b/tests/Doctrine/Tests/Mocks/ConnectionMock.php @@ -8,7 +8,7 @@ class ConnectionMock extends \Doctrine\DBAL\Connection private $_platformMock; private $_lastInsertId = 0; private $_inserts = array(); - + public function __construct(array $params, $driver, $config = null, $eventManager = null) { $this->_platformMock = new DatabasePlatformMock(); @@ -18,7 +18,7 @@ class ConnectionMock extends \Doctrine\DBAL\Connection // Override possible assignment of platform to database platform mock $this->_platform = $this->_platformMock; } - + /** * @override */ @@ -26,15 +26,15 @@ class ConnectionMock extends \Doctrine\DBAL\Connection { return $this->_platformMock; } - + /** * @override */ - public function insert($tableName, array $data) + public function insert($tableName, array $data, array $types = array()) { $this->_inserts[$tableName][] = $data; } - + /** * @override */ @@ -50,7 +50,7 @@ class ConnectionMock extends \Doctrine\DBAL\Connection { return $this->_fetchOneResult; } - + /** * @override */ @@ -61,29 +61,29 @@ class ConnectionMock extends \Doctrine\DBAL\Connection } return $input; } - + /* Mock API */ public function setFetchOneResult($fetchOneResult) { $this->_fetchOneResult = $fetchOneResult; } - + public function setDatabasePlatform($platform) { $this->_platformMock = $platform; } - + public function setLastInsertId($id) { $this->_lastInsertId = $id; } - + public function getInserts() { return $this->_inserts; } - + public function reset() { $this->_inserts = array(); diff --git a/tests/Doctrine/Tests/Mocks/DatabasePlatformMock.php b/tests/Doctrine/Tests/Mocks/DatabasePlatformMock.php index b2954cf55..b634408be 100644 --- a/tests/Doctrine/Tests/Mocks/DatabasePlatformMock.php +++ b/tests/Doctrine/Tests/Mocks/DatabasePlatformMock.php @@ -57,7 +57,7 @@ class DatabasePlatformMock extends \Doctrine\DBAL\Platforms\AbstractPlatform /** @override */ public function getVarcharTypeDeclarationSQL(array $field) {} - + /** @override */ public function getClobTypeDeclarationSQL(array $field) {} @@ -85,6 +85,13 @@ class DatabasePlatformMock extends \Doctrine\DBAL\Platforms\AbstractPlatform protected function initializeDoctrineTypeMappings() { - + + } + /** + * Gets the SQL Snippet used to declare a BLOB column type. + */ + public function getBlobTypeDeclarationSQL(array $field) + { + throw DBALException::notSupported(__METHOD__); } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/Mocks/HydratorMockStatement.php b/tests/Doctrine/Tests/Mocks/HydratorMockStatement.php index 555982765..b5f5e3b47 100644 --- a/tests/Doctrine/Tests/Mocks/HydratorMockStatement.php +++ b/tests/Doctrine/Tests/Mocks/HydratorMockStatement.php @@ -8,10 +8,10 @@ namespace Doctrine\Tests\Mocks; * * @author Roman Borschel */ -class HydratorMockStatement implements \Doctrine\DBAL\Driver\Statement +class HydratorMockStatement implements \IteratorAggregate, \Doctrine\DBAL\Driver\Statement { - private $_resultSet; - + private $_resultSet; + /** * Creates a new mock statement that will serve the provided fake result set to clients. * @@ -21,7 +21,7 @@ class HydratorMockStatement implements \Doctrine\DBAL\Driver\Statement { $this->_resultSet = $resultSet; } - + /** * Fetches all rows from the result set. * @@ -31,7 +31,7 @@ class HydratorMockStatement implements \Doctrine\DBAL\Driver\Statement { return $this->_resultSet; } - + public function fetchColumn($columnNumber = 0) { $row = current($this->_resultSet); @@ -39,10 +39,10 @@ class HydratorMockStatement implements \Doctrine\DBAL\Driver\Statement $val = array_shift($row); return $val !== null ? $val : false; } - + /** * Fetches the next row in the result set. - * + * */ public function fetch($fetchStyle = null) { @@ -50,7 +50,7 @@ class HydratorMockStatement implements \Doctrine\DBAL\Driver\Statement next($this->_resultSet); return $current; } - + /** * Closes the cursor, enabling the statement to be executed again. * @@ -60,13 +60,13 @@ class HydratorMockStatement implements \Doctrine\DBAL\Driver\Statement { return true; } - + public function setResultSet(array $resultSet) { reset($resultSet); $this->_resultSet = $resultSet; } - + public function bindColumn($column, &$param, $type = null) { } @@ -78,7 +78,7 @@ class HydratorMockStatement implements \Doctrine\DBAL\Driver\Statement public function bindParam($column, &$variable, $type = null, $length = null, $driverOptions = array()) { } - + public function columnCount() { } @@ -86,16 +86,26 @@ class HydratorMockStatement implements \Doctrine\DBAL\Driver\Statement public function errorCode() { } - + public function errorInfo() { } - + public function execute($params = array()) { } - + public function rowCount() { - } + } + + public function getIterator() + { + return $this->_resultSet; + } + + public function setFetchMode($fetchMode) + { + + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/CMS/CmsAddress.php b/tests/Doctrine/Tests/Models/CMS/CmsAddress.php index 9db2b6475..d32416a5e 100644 --- a/tests/Doctrine/Tests/Models/CMS/CmsAddress.php +++ b/tests/Doctrine/Tests/Models/CMS/CmsAddress.php @@ -46,7 +46,7 @@ class CmsAddress public function getId() { return $this->id; } - + public function getUser() { return $this->user; } @@ -62,7 +62,7 @@ class CmsAddress public function getCity() { return $this->city; } - + public function setUser(CmsUser $user) { if ($this->user !== $user) { $this->user = $user; diff --git a/tests/Doctrine/Tests/Models/DDC117/DDC117Article.php b/tests/Doctrine/Tests/Models/DDC117/DDC117Article.php index 51ea2278d..70b1c6353 100644 --- a/tests/Doctrine/Tests/Models/DDC117/DDC117Article.php +++ b/tests/Doctrine/Tests/Models/DDC117/DDC117Article.php @@ -9,6 +9,7 @@ class DDC117Article { /** @Id @Column(type="integer", name="article_id") @GeneratedValue */ private $id; + /** @Column */ private $title; diff --git a/tests/Doctrine/Tests/Models/DDC1476/DDC1476EntityWithDefaultFieldType.php b/tests/Doctrine/Tests/Models/DDC1476/DDC1476EntityWithDefaultFieldType.php new file mode 100644 index 000000000..f06199b4d --- /dev/null +++ b/tests/Doctrine/Tests/Models/DDC1476/DDC1476EntityWithDefaultFieldType.php @@ -0,0 +1,76 @@ +. + */ + +namespace Doctrine\Tests\Models\DDC1476; + +/** + * @Entity() + */ +class DDC1476EntityWithDefaultFieldType +{ + + /** + * @Id + * @Column() + * @GeneratedValue("NONE") + */ + protected $id; + + /** @column() */ + protected $name; + + /** + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + public static function loadMetadata(\Doctrine\ORM\Mapping\ClassMetadataInfo $metadata) + { + $metadata->mapField(array( + 'id' => true, + 'fieldName' => 'id', + )); + $metadata->mapField(array( + 'fieldName' => 'name', + )); + + $metadata->setIdGeneratorType(\Doctrine\ORM\Mapping\ClassMetadataInfo::GENERATOR_TYPE_NONE); + } + +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Legacy/LegacyUser.php b/tests/Doctrine/Tests/Models/Legacy/LegacyUser.php index f4f5e1fcb..3e3deedb1 100644 --- a/tests/Doctrine/Tests/Models/Legacy/LegacyUser.php +++ b/tests/Doctrine/Tests/Models/Legacy/LegacyUser.php @@ -21,7 +21,7 @@ class LegacyUser */ public $_username; /** - * @Column(type="string", length=255) + * @Column(type="string", length=255, name="name") */ public $_name; /** diff --git a/tests/Doctrine/Tests/Models/Legacy/LegacyUserReference.php b/tests/Doctrine/Tests/Models/Legacy/LegacyUserReference.php index c6cd891a1..e666ae196 100644 --- a/tests/Doctrine/Tests/Models/Legacy/LegacyUserReference.php +++ b/tests/Doctrine/Tests/Models/Legacy/LegacyUserReference.php @@ -23,12 +23,12 @@ class LegacyUserReference private $_target; /** - * @column(type="string") + * @column(type="string", name="description") */ private $_description; /** - * @column(type="datetime") + * @column(type="datetime", name="created") */ private $_created; diff --git a/tests/Doctrine/Tests/ORM/Functional/CustomTreeWalkersTest.php b/tests/Doctrine/Tests/ORM/Functional/CustomTreeWalkersTest.php index ada41d00b..72fc6d587 100644 --- a/tests/Doctrine/Tests/ORM/Functional/CustomTreeWalkersTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/CustomTreeWalkersTest.php @@ -1,7 +1,5 @@ useModelSet('cms'); - parent::setUp(); + private $_em; + + protected function setUp() + { + $this->_em = $this->_getTestEntityManager(); } public function assertSqlGeneration($dqlToBeTested, $sqlToBeConfirmed) @@ -70,7 +70,7 @@ class CustomTreeWalkersTest extends \Doctrine\Tests\OrmFunctionalTestCase "SELECT c0_.id AS id0, c0_.status AS status1, c0_.username AS username2, c0_.name AS name3, c0_.email_id AS email_id4 FROM cms_users c0_ WHERE (c0_.name = ? OR c0_.name = ?) AND c0_.id = 1" ); } - + public function testSupportsQueriesWithSimpleConditionalExpression() { $this->assertSqlGeneration( @@ -94,7 +94,7 @@ class CustomTreeWalker extends Query\TreeWalkerAdapter $dqlAliases[] = $dqlAlias; } } - + // Create our conditions for all involved classes $factors = array(); foreach ($dqlAliases as $alias) { @@ -108,7 +108,7 @@ class CustomTreeWalker extends Query\TreeWalkerAdapter $factor = new Query\AST\ConditionalFactor($condPrimary); $factors[] = $factor; } - + if (($whereClause = $selectStatement->whereClause) !== null) { // There is already a WHERE clause, so append the conditions $condExpr = $whereClause->conditionalExpression; @@ -119,18 +119,18 @@ class CustomTreeWalker extends Query\TreeWalkerAdapter $whereClause->conditionalExpression = $condExpr; } - + $existingTerms = $whereClause->conditionalExpression->conditionalTerms; - + if (count($existingTerms) > 1) { // More than one term, so we need to wrap all these terms in a single root term // i.e: "WHERE u.name = :foo or u.other = :bar" => "WHERE (u.name = :foo or u.other = :bar) AND " - + $primary = new Query\AST\ConditionalPrimary; $primary->conditionalExpression = new Query\AST\ConditionalExpression($existingTerms); $existingFactor = new Query\AST\ConditionalFactor($primary); $term = new Query\AST\ConditionalTerm(array_merge(array($existingFactor), $factors)); - + $selectStatement->whereClause->conditionalExpression->conditionalTerms = array($term); } else { // Just one term so we can simply append our factors to that term diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index 4f8e11420..719a9f993 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -20,7 +20,9 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase public function tearDown() { - $this->_em->getConfiguration()->setEntityNamespaces(array()); + if ($this->_em) { + $this->_em->getConfiguration()->setEntityNamespaces(array()); + } parent::tearDown(); } @@ -78,7 +80,7 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase return array($user->id, $address->id); } - + public function buildUser($name, $username, $status, $address) { $user = new CmsUser(); @@ -89,10 +91,10 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->persist($user); $this->_em->flush(); - + return $user; } - + public function buildAddress($country, $city, $street, $zip) { $address = new CmsAddress(); @@ -103,7 +105,7 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->persist($address); $this->_em->flush(); - + return $address; } @@ -134,22 +136,22 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase { $address1 = $this->buildAddress('Germany', 'Berlim', 'Foo st.', '123456'); $user1 = $this->buildUser('Benjamin', 'beberlei', 'dev', $address1); - + $address2 = $this->buildAddress('Brazil', 'São Paulo', 'Bar st.', '654321'); $user2 = $this->buildUser('Guilherme', 'guilhermeblanco', 'freak', $address2); - + $address3 = $this->buildAddress('USA', 'Nashville', 'Woo st.', '321654'); $user3 = $this->buildUser('Jonathan', 'jwage', 'dev', $address3); - + unset($address1); unset($address2); unset($address3); - + $this->_em->clear(); - + $repository = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsAddress'); $addresses = $repository->findBy(array('user' => array($user1->getId(), $user2->getId()))); - + $this->assertEquals(2, count($addresses)); $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsAddress',$addresses[0]); } @@ -158,22 +160,22 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase { $address1 = $this->buildAddress('Germany', 'Berlim', 'Foo st.', '123456'); $user1 = $this->buildUser('Benjamin', 'beberlei', 'dev', $address1); - + $address2 = $this->buildAddress('Brazil', 'São Paulo', 'Bar st.', '654321'); $user2 = $this->buildUser('Guilherme', 'guilhermeblanco', 'freak', $address2); - + $address3 = $this->buildAddress('USA', 'Nashville', 'Woo st.', '321654'); $user3 = $this->buildUser('Jonathan', 'jwage', 'dev', $address3); - + unset($address1); unset($address2); unset($address3); - + $this->_em->clear(); - + $repository = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsAddress'); $addresses = $repository->findBy(array('user' => array($user1, $user2))); - + $this->assertEquals(2, count($addresses)); $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsAddress',$addresses[0]); } @@ -189,7 +191,7 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals('Guilherme', $users[0]->name); $this->assertEquals('dev', $users[0]->status); } - + public function testFindAll() { $user1Id = $this->loadFixture(); @@ -280,7 +282,7 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $userId = $user->id; $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); } @@ -423,7 +425,7 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase public function testFindByLimitOffset() { $this->loadFixture(); - + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); $users1 = $repos->findBy(array(), null, 1, 0); @@ -451,8 +453,8 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertSame($usersAsc[0], $usersDesc[2]); $this->assertSame($usersAsc[2], $usersDesc[0]); } - - + + /** * @group DDC-753 */ @@ -465,19 +467,19 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $repos = $this->_em->getRepository('Doctrine\Tests\Models\DDC753\DDC753EntityWithDefaultCustomRepository'); $this->assertInstanceOf("Doctrine\Tests\Models\DDC753\DDC753DefaultRepository", $repos); $this->assertTrue($repos->isDefaultRepository()); - - + + $repos = $this->_em->getRepository('Doctrine\Tests\Models\DDC753\DDC753EntityWithCustomRepository'); $this->assertInstanceOf("Doctrine\Tests\Models\DDC753\DDC753CustomRepository", $repos); $this->assertTrue($repos->isCustomRepository()); - + $this->assertEquals($this->_em->getConfiguration()->getDefaultRepositoryClassName(), "Doctrine\Tests\Models\DDC753\DDC753DefaultRepository"); $this->_em->getConfiguration()->setDefaultRepositoryClassName("Doctrine\ORM\EntityRepository"); $this->assertEquals($this->_em->getConfiguration()->getDefaultRepositoryClassName(), "Doctrine\ORM\EntityRepository"); } - - + + /** * @group DDC-753 * @expectedException Doctrine\ORM\ORMException @@ -488,6 +490,6 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals($this->_em->getConfiguration()->getDefaultRepositoryClassName(), "Doctrine\ORM\EntityRepository"); $this->_em->getConfiguration()->setDefaultRepositoryClassName("Doctrine\Tests\Models\DDC753\DDC753InvalidRepository"); } - + } diff --git a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php index 4bf010602..351c1e25b 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -29,7 +29,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsGroup'); $class->associationMappings['users']['fetch'] = ClassMetadataInfo::FETCH_EXTRA_LAZY; - + $this->loadFixture(); } @@ -137,9 +137,9 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase { $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); $this->assertFalse($user->groups->isInitialized(), "Pre-Condition: Collection is not initialized."); - + $queryCount = $this->getCurrentQueryCount(); - + $someGroups = $user->groups->slice(0, 2); $this->assertContainsOnly('Doctrine\Tests\Models\CMS\CmsGroup', $someGroups); @@ -225,7 +225,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertFalse($user->articles->isInitialized(), "Pre-Condition: Collection is not initialized."); $article = $this->_em->find('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId); - + $queryCount = $this->getCurrentQueryCount(); $this->assertTrue($user->articles->contains($article)); $this->assertFalse($user->articles->isInitialized(), "Post-Condition: Collection is not initialized."); @@ -304,6 +304,49 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertFalse($user->groups->isInitialized(), "Post-Condition: Collection is not initialized."); } + /** + * @group DDC-1399 + */ + public function testCountAfterAddThenFlush() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + + $newGroup = new \Doctrine\Tests\Models\CMS\CmsGroup(); + $newGroup->name = "Test4"; + + $user->addGroup($newGroup); + $this->_em->persist($newGroup); + + $this->assertFalse($user->groups->isInitialized()); + $this->assertEquals(4, count($user->groups)); + $this->assertFalse($user->groups->isInitialized()); + + $this->_em->flush(); + + $this->assertEquals(4, count($user->groups)); + } + + /** + * @group DDC-1462 + */ + public function testSliceOnDirtyCollection() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + /* @var $user CmsUser */ + + $newGroup = new \Doctrine\Tests\Models\CMS\CmsGroup(); + $newGroup->name = "Test4"; + + $user->addGroup($newGroup); + $this->_em->persist($newGroup); + + $qc = $this->getCurrentQueryCount(); + $groups = $user->groups->slice(0, 10); + + $this->assertEquals(4, count($groups)); + $this->assertEquals($qc + 1, $this->getCurrentQueryCount()); + } + private function loadFixture() { $user1 = new \Doctrine\Tests\Models\CMS\CmsUser(); @@ -364,7 +407,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->persist($article1); $this->_em->persist($article2); - + $this->_em->flush(); $this->_em->clear(); diff --git a/tests/Doctrine/Tests/ORM/Functional/LifecycleCallbackTest.php b/tests/Doctrine/Tests/ORM/Functional/LifecycleCallbackTest.php index 2d71541d2..8015f341f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/LifecycleCallbackTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/LifecycleCallbackTest.php @@ -43,6 +43,29 @@ class LifecycleCallbackTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals('changed from preUpdate callback!', $result[0]->value); } + public function testPreFlushCallbacksAreInvoked() + { + $entity = new LifecycleCallbackTestEntity; + $entity->value = 'hello'; + $this->_em->persist($entity); + + $this->_em->flush(); + + $this->assertTrue($entity->prePersistCallbackInvoked); + $this->assertTrue($entity->preFlushCallbackInvoked); + + $entity->preFlushCallbackInvoked = false; + $this->_em->flush(); + + $this->assertTrue($entity->preFlushCallbackInvoked); + + $entity->value = 'bye'; + $entity->preFlushCallbackInvoked = false; + $this->_em->flush(); + + $this->assertTrue($entity->preFlushCallbackInvoked); + } + public function testChangesDontGetLost() { $user = new LifecycleCallbackTestUser; @@ -190,6 +213,8 @@ class LifecycleCallbackTestEntity public $postPersistCallbackInvoked = false; public $postLoadCallbackInvoked = false; + public $preFlushCallbackInvoked = false; + /** * @Id @Column(type="integer") * @GeneratedValue(strategy="AUTO") @@ -233,6 +258,11 @@ class LifecycleCallbackTestEntity public function doStuffOnPreUpdate() { $this->value = 'changed from preUpdate callback!'; } + + /** @PreFlush */ + public function doStuffOnPreFlush() { + $this->preFlushCallbackInvoked = true; + } } /** diff --git a/tests/Doctrine/Tests/ORM/Functional/MappedSuperclassTest.php b/tests/Doctrine/Tests/ORM/Functional/MappedSuperclassTest.php index bb324bf67..f4439b9ad 100644 --- a/tests/Doctrine/Tests/ORM/Functional/MappedSuperclassTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/MappedSuperclassTest.php @@ -39,6 +39,7 @@ class MappedSuperclassTest extends \Doctrine\Tests\OrmFunctionalTestCase $cleanFile = $this->_em->find(get_class($file), $file->getId()); $this->assertInstanceOf('Doctrine\Tests\Models\DirectoryTree\Directory', $cleanFile->getParent()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $cleanFile->getParent()); $this->assertEquals($directory->getId(), $cleanFile->getParent()->getId()); $this->assertInstanceOf('Doctrine\Tests\Models\DirectoryTree\Directory', $cleanFile->getParent()->getParent()); $this->assertEquals($root->getId(), $cleanFile->getParent()->getParent()->getId()); diff --git a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php index 3c41e0201..eb46329f1 100644 --- a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php @@ -35,7 +35,7 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $user->status = 'dev'; $this->_em->persist($user); $this->_em->flush(); - + $this->_em->clear(); $rsm = new ResultSetMapping; @@ -94,24 +94,24 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals($addr->street, $addresses[0]->street); $this->assertTrue($addresses[0]->user instanceof CmsUser); } - + public function testJoinedOneToManyNativeQuery() { $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 ResultSetMapping; $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); $rsm->addFieldResult('u', $this->platform->getSQLResultCasing('id'), 'id'); @@ -119,7 +119,7 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $rsm->addFieldResult('u', $this->platform->getSQLResultCasing('status'), 'status'); $rsm->addJoinedEntityResult('Doctrine\Tests\Models\CMS\CmsPhonenumber', 'p', 'u', 'phonenumbers'); $rsm->addFieldResult('p', $this->platform->getSQLResultCasing('phonenumber'), 'phonenumber'); - + $query = $this->_em->createNativeQuery('SELECT id, name, status, phonenumber FROM cms_users INNER JOIN cms_phonenumbers ON id = user_id WHERE username = ?', $rsm); $query->setParameter(1, 'romanb'); @@ -133,30 +133,30 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $phones = $users[0]->getPhonenumbers(); $this->assertEquals(424242, $phones[0]->phonenumber); $this->assertTrue($phones[0]->getUser() === $users[0]); - + } - + public function testJoinedOneToOneNativeQuery() { $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 ResultSetMapping; $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); $rsm->addFieldResult('u', $this->platform->getSQLResultCasing('id'), 'id'); @@ -167,12 +167,12 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $rsm->addFieldResult('a', $this->platform->getSQLResultCasing('country'), 'country'); $rsm->addFieldResult('a', $this->platform->getSQLResultCasing('zip'), 'zip'); $rsm->addFieldResult('a', $this->platform->getSQLResultCasing('city'), 'city'); - + $query = $this->_em->createNativeQuery('SELECT u.id, u.name, u.status, a.id AS a_id, a.country, a.zip, a.city 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->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $users[0]); $this->assertEquals('Roman', $users[0]->name); diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToManyUnidirectionalAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToManyUnidirectionalAssociationTest.php index fd7a22a5a..42fd29235 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToManyUnidirectionalAssociationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToManyUnidirectionalAssociationTest.php @@ -73,7 +73,14 @@ class OneToManyUnidirectionalAssociationTest extends \Doctrine\Tests\OrmFunction $this->_em->persist($routeA); $this->_em->persist($routeB); - $this->setExpectedException('Exception'); // depends on the underyling Database Driver - $this->_em->flush(); // Exception + $exceptionThrown = false; + try { + // exception depending on the underyling Database Driver + $this->_em->flush(); + } catch(\Exception $e) { + $exceptionThrown = true; + } + + $this->assertTrue($exceptionThrown, "The underlying database driver throws an exception."); } -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php index 044a17381..5fa55036c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -19,6 +19,7 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $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\TrainOwner'), $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\Waggon'), )); } catch(\Exception $e) {} @@ -26,7 +27,7 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase public function testEagerLoadOneToOneOwningSide() { - $train = new Train(); + $train = new Train(new TrainOwner("Alexander")); $driver = new TrainDriver("Benjamin"); $waggon = new Waggon(); @@ -48,7 +49,7 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase public function testEagerLoadOneToOneNullOwningSide() { - $train = new Train(); + $train = new Train(new TrainOwner("Alexander")); $this->_em->persist($train); // cascades $this->_em->flush(); @@ -65,9 +66,8 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase public function testEagerLoadOneToOneInverseSide() { - $train = new Train(); - $driver = new TrainDriver("Benjamin"); - $train->setDriver($driver); + $owner = new TrainOwner("Alexander"); + $train = new Train($owner); $this->_em->persist($train); // cascades $this->_em->flush(); @@ -75,9 +75,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $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); + $driver = $this->_em->find(get_class($owner), $owner->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $owner->train); + $this->assertNotNull($owner->train); $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); } @@ -103,7 +103,7 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase public function testEagerLoadManyToOne() { - $train = new Train(); + $train = new Train(new TrainOwner("Alexander")); $waggon = new Waggon(); $train->addWaggon($waggon); @@ -115,6 +115,59 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $waggon->train); $this->assertNotNull($waggon->train); } + + public function testEagerLoadWithNullableColumnsGeneratesLeftJoinOnBothSides() + { + $train = new Train(new TrainOwner("Alexander")); + $driver = new TrainDriver("Benjamin"); + $train->setDriver($driver); + + $this->_em->persist($train); + $this->_em->flush(); + $this->_em->clear(); + + $train = $this->_em->find(get_class($train), $train->id); + $this->assertEquals( + "SELECT t0.id AS id1, t0.driver_id AS driver_id2, t3.id AS id4, t3.name AS name5, t0.owner_id AS owner_id6, t7.id AS id8, t7.name AS name9 FROM Train t0 LEFT JOIN TrainDriver t3 ON t0.driver_id = t3.id INNER JOIN TrainOwner t7 ON t0.owner_id = t7.id WHERE t0.id = ?", + $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery]['sql'] + ); + + $this->_em->clear(); + $driver = $this->_em->find(get_class($driver), $driver->id); + $this->assertEquals( + "SELECT t0.id AS id1, t0.name AS name2, t3.id AS id4, t3.driver_id AS driver_id5, t3.owner_id AS owner_id6 FROM TrainOwner t0 LEFT JOIN Train t3 ON t3.owner_id = t0.id WHERE t0.id IN (?)", + $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery]['sql'] + ); + } + + public function testEagerLoadWithNonNullableColumnsGeneratesInnerJoinOnOwningSide() + { + $waggon = new Waggon(); + $this->_em->persist($waggon); + $this->_em->flush(); + $this->_em->clear(); + + $waggon = $this->_em->find(get_class($waggon), $waggon->id); + $this->assertEquals( + "SELECT t0.id AS id1, t0.train_id AS train_id2, t3.id AS id4, t3.driver_id AS driver_id5, t3.owner_id AS owner_id6 FROM Waggon t0 INNER JOIN Train t3 ON t0.train_id = t3.id WHERE t0.id = ?", + $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery]['sql'] + ); + } + + public function testEagerLoadWithNonNullableColumnsGeneratesLeftJoinOnNonOwningSide() + { + $owner = new TrainOwner('Alexander'); + $train = new Train($owner); + $this->_em->persist($train); + $this->_em->flush(); + $this->_em->clear(); + + $waggon = $this->_em->find(get_class($owner), $owner->id); + $this->assertEquals( + "SELECT t0.id AS id1, t0.name AS name2, t3.id AS id4, t3.driver_id AS driver_id5, t3.owner_id AS owner_id6 FROM TrainOwner t0 LEFT JOIN Train t3 ON t3.owner_id = t0.id WHERE t0.id = ?", + $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery]['sql'] + ); + } } /** @@ -130,16 +183,23 @@ class Train /** * Owning side * @OneToOne(targetEntity="TrainDriver", inversedBy="train", fetch="EAGER", cascade={"persist"}) + * @JoinColumn(nullable=true) */ public $driver; + /** + * Owning side + * @OneToOne(targetEntity="TrainOwner", inversedBy="train", fetch="EAGER", cascade={"persist"}) + */ + public $owner; /** * @oneToMany(targetEntity="Waggon", mappedBy="train", cascade={"persist"}) */ public $waggons; - public function __construct() + public function __construct(TrainOwner $owner) { $this->waggons = new \Doctrine\Common\Collections\ArrayCollection(); + $this->setOwner($owner); } public function setDriver(TrainDriver $driver) @@ -148,6 +208,12 @@ class Train $driver->setTrain($this); } + public function setOwner(TrainOwner $owner) + { + $this->owner = $owner; + $owner->setTrain($this); + } + public function addWaggon(Waggon $w) { $w->setTrain($this); @@ -181,6 +247,32 @@ class TrainDriver } } +/** + * @Entity + */ +class TrainOwner +{ + /** @Id @Column(type="integer") @GeneratedValue */ + public $id; + /** @column(type="string") */ + public $name; + /** + * Inverse side + * @OneToOne(targetEntity="Train", mappedBy="owner", fetch="EAGER") + */ + public $train; + + public function __construct($name) + { + $this->name = $name; + } + + public function setTrain(Train $t) + { + $this->train = $t; + } +} + /** * @Entity */ @@ -195,4 +287,4 @@ class Waggon { $this->train = $train; } -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/ORM/Functional/ReadOnlyTest.php b/tests/Doctrine/Tests/ORM/Functional/ReadOnlyTest.php index 8a5819956..44832fc76 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ReadOnlyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ReadOnlyTest.php @@ -27,14 +27,14 @@ class ReadOnlyTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->flush(); $readOnly->name = "Test2"; - $readOnly->number = 4321; + $readOnly->numericValue = 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); + $this->assertEquals(1234, $dbReadOnly->numericValue); } } @@ -51,11 +51,11 @@ class ReadOnlyEntity /** @column(type="string") */ public $name; /** @Column(type="integer") */ - public $number; + public $numericValue; public function __construct($name, $number) { $this->name = $name; - $this->number = $number; + $this->numericValue = $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 b89f3d04e..9cd216066 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php @@ -5,6 +5,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\ORM\Proxy\ProxyFactory; use Doctrine\ORM\Proxy\ProxyClassGenerator; use Doctrine\Tests\Models\ECommerce\ECommerceProduct; +use Doctrine\Tests\Models\ECommerce\ECommerceShipping; require_once __DIR__ . '/../../TestInit.php'; @@ -97,7 +98,7 @@ class ReferenceProxyTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertTrue($clone->isCloned); $this->assertFalse($entity->isCloned); } - + /** * @group DDC-733 */ @@ -107,12 +108,12 @@ class ReferenceProxyTest extends \Doctrine\Tests\OrmFunctionalTestCase /* @var $entity Doctrine\Tests\Models\ECommerce\ECommerceProduct */ $entity = $this->_em->getReference('Doctrine\Tests\Models\ECommerce\ECommerceProduct' , $id); - + $this->assertFalse($entity->__isInitialized__, "Pre-Condition: Object is unitialized proxy."); $this->_em->getUnitOfWork()->initializeObject($entity); $this->assertTrue($entity->__isInitialized__, "Should be initialized after called UnitOfWork::initializeObject()"); } - + /** * @group DDC-1163 */ @@ -123,10 +124,10 @@ class ReferenceProxyTest extends \Doctrine\Tests\OrmFunctionalTestCase /* @var $entity Doctrine\Tests\Models\ECommerce\ECommerceProduct */ $entity = $this->_em->getReference('Doctrine\Tests\Models\ECommerce\ECommerceProduct' , $id); $entity->setName('Doctrine 2 Cookbook'); - + $this->_em->flush(); $this->_em->clear(); - + $entity = $this->_em->getReference('Doctrine\Tests\Models\ECommerce\ECommerceProduct' , $id); $this->assertEquals('Doctrine 2 Cookbook', $entity->getName()); } @@ -160,6 +161,29 @@ class ReferenceProxyTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertFalse($entity->__isInitialized__, "Getting the identifier doesn't initialize the proxy."); } + public function testDoNotInitializeProxyOnGettingTheIdentifierAndReturnTheRightType() + { + $product = new ECommerceProduct(); + $product->setName('Doctrine Cookbook'); + + $shipping = new ECommerceShipping(); + $shipping->setDays(1); + $product->setShipping($shipping); + $this->_em->persist($product); + $this->_em->flush(); + $this->_em->clear(); + + $id = $shipping->getId(); + + $product = $this->_em->getRepository('Doctrine\Tests\Models\ECommerce\ECommerceProduct')->find($product->getId()); + + $entity = $product->getShipping(); + $this->assertFalse($entity->__isInitialized__, "Pre-Condition: Object is unitialized proxy."); + $this->assertEquals($id, $entity->getId()); + $this->assertSame($id, $entity->getId(), "Check that the id's are the same value, and type."); + $this->assertFalse($entity->__isInitialized__, "Getting the identifier doesn't initialize the proxy."); + } + public function testInitializeProxyOnGettingSomethingOtherThanTheIdentifier() { $id = $this->createProduct(); diff --git a/tests/Doctrine/Tests/ORM/Functional/SchemaTool/PostgreSqlSchemaToolTest.php b/tests/Doctrine/Tests/ORM/Functional/SchemaTool/PostgreSqlSchemaToolTest.php index b93753ce0..79e7af99a 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SchemaTool/PostgreSqlSchemaToolTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SchemaTool/PostgreSqlSchemaToolTest.php @@ -21,7 +21,7 @@ class PostgreSqlSchemaToolTest extends \Doctrine\Tests\OrmFunctionalTestCase $address = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress'); $this->assertEquals(1, $address->sequenceGeneratorDefinition['allocationSize']); } - + public function testGetCreateSchemaSql() { $classes = array( @@ -32,26 +32,30 @@ class PostgreSqlSchemaToolTest extends \Doctrine\Tests\OrmFunctionalTestCase $tool = new SchemaTool($this->_em); $sql = $tool->getCreateSchemaSql($classes); - - $this->assertEquals("CREATE TABLE cms_addresses (id INT NOT NULL, user_id INT DEFAULT NULL, country VARCHAR(50) NOT NULL, zip VARCHAR(50) NOT NULL, city VARCHAR(50) NOT NULL, PRIMARY KEY(id))", $sql[0]); - $this->assertEquals("CREATE UNIQUE INDEX UNIQ_ACAC157BA76ED395 ON cms_addresses (user_id)", $sql[1]); - $this->assertEquals("CREATE TABLE cms_users (id INT NOT NULL, status VARCHAR(50) NOT NULL, username VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))", $sql[2]); - $this->assertEquals("CREATE UNIQUE INDEX UNIQ_3AF03EC5F85E0677 ON cms_users (username)", $sql[3]); - $this->assertEquals("CREATE TABLE cms_users_groups (user_id INT NOT NULL, group_id INT NOT NULL, PRIMARY KEY(user_id, group_id))", $sql[4]); - $this->assertEquals("CREATE INDEX IDX_7EA9409AA76ED395 ON cms_users_groups (user_id)", $sql[5]); - $this->assertEquals("CREATE INDEX IDX_7EA9409AFE54D947 ON cms_users_groups (group_id)", $sql[6]); - $this->assertEquals("CREATE TABLE cms_phonenumbers (phonenumber VARCHAR(50) NOT NULL, user_id INT DEFAULT NULL, PRIMARY KEY(phonenumber))", $sql[7]); - $this->assertEquals("CREATE INDEX IDX_F21F790FA76ED395 ON cms_phonenumbers (user_id)", $sql[8]); - $this->assertEquals("CREATE SEQUENCE cms_addresses_id_seq INCREMENT BY 1 MINVALUE 1 START 1", $sql[9]); - $this->assertEquals("CREATE SEQUENCE cms_users_id_seq INCREMENT BY 1 MINVALUE 1 START 1", $sql[10]); - $this->assertEquals("ALTER TABLE cms_addresses ADD FOREIGN KEY (user_id) REFERENCES cms_users(id) NOT DEFERRABLE INITIALLY IMMEDIATE", $sql[11]); - $this->assertEquals("ALTER TABLE cms_users_groups ADD FOREIGN KEY (user_id) REFERENCES cms_users(id) NOT DEFERRABLE INITIALLY IMMEDIATE", $sql[12]); - $this->assertEquals("ALTER TABLE cms_users_groups ADD FOREIGN KEY (group_id) REFERENCES cms_groups(id) NOT DEFERRABLE INITIALLY IMMEDIATE", $sql[13]); - $this->assertEquals("ALTER TABLE cms_phonenumbers ADD FOREIGN KEY (user_id) REFERENCES cms_users(id) NOT DEFERRABLE INITIALLY IMMEDIATE", $sql[14]); - - $this->assertEquals(count($sql), 15); + $sqlCount = count($sql); + + $this->assertEquals("CREATE TABLE cms_addresses (id INT NOT NULL, user_id INT DEFAULT NULL, country VARCHAR(50) NOT NULL, zip VARCHAR(50) NOT NULL, city VARCHAR(50) NOT NULL, PRIMARY KEY(id))", array_shift($sql)); + $this->assertEquals("CREATE UNIQUE INDEX UNIQ_ACAC157BA76ED395 ON cms_addresses (user_id)", array_shift($sql)); + $this->assertEquals("CREATE TABLE cms_users (id INT NOT NULL, email_id INT DEFAULT NULL, status VARCHAR(50) DEFAULT NULL, username VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))", array_shift($sql)); + $this->assertEquals("CREATE UNIQUE INDEX UNIQ_3AF03EC5F85E0677 ON cms_users (username)", array_shift($sql)); + $this->assertEquals("CREATE UNIQUE INDEX UNIQ_3AF03EC5A832C1C9 ON cms_users (email_id)", array_shift($sql)); + $this->assertEquals("CREATE TABLE cms_users_groups (user_id INT NOT NULL, group_id INT NOT NULL, PRIMARY KEY(user_id, group_id))", array_shift($sql)); + $this->assertEquals("CREATE INDEX IDX_7EA9409AA76ED395 ON cms_users_groups (user_id)", array_shift($sql)); + $this->assertEquals("CREATE INDEX IDX_7EA9409AFE54D947 ON cms_users_groups (group_id)", array_shift($sql)); + $this->assertEquals("CREATE TABLE cms_phonenumbers (phonenumber VARCHAR(50) NOT NULL, user_id INT DEFAULT NULL, PRIMARY KEY(phonenumber))", array_shift($sql)); + $this->assertEquals("CREATE INDEX IDX_F21F790FA76ED395 ON cms_phonenumbers (user_id)", array_shift($sql)); + $this->assertEquals("CREATE SEQUENCE cms_addresses_id_seq INCREMENT BY 1 MINVALUE 1 START 1", array_shift($sql)); + $this->assertEquals("CREATE SEQUENCE cms_users_id_seq INCREMENT BY 1 MINVALUE 1 START 1", array_shift($sql)); + $this->assertEquals("ALTER TABLE cms_addresses ADD CONSTRAINT FK_ACAC157BA76ED395 FOREIGN KEY (user_id) REFERENCES cms_users (id) NOT DEFERRABLE INITIALLY IMMEDIATE", array_shift($sql)); + $this->assertEquals("ALTER TABLE cms_users ADD CONSTRAINT FK_3AF03EC5A832C1C9 FOREIGN KEY (email_id) REFERENCES cms_emails (id) NOT DEFERRABLE INITIALLY IMMEDIATE", array_shift($sql)); + $this->assertEquals("ALTER TABLE cms_users_groups ADD CONSTRAINT FK_7EA9409AA76ED395 FOREIGN KEY (user_id) REFERENCES cms_users (id) NOT DEFERRABLE INITIALLY IMMEDIATE", array_shift($sql)); + $this->assertEquals("ALTER TABLE cms_users_groups ADD CONSTRAINT FK_7EA9409AFE54D947 FOREIGN KEY (group_id) REFERENCES cms_groups (id) NOT DEFERRABLE INITIALLY IMMEDIATE", array_shift($sql)); + $this->assertEquals("ALTER TABLE cms_phonenumbers ADD CONSTRAINT FK_F21F790FA76ED395 FOREIGN KEY (user_id) REFERENCES cms_users (id) NOT DEFERRABLE INITIALLY IMMEDIATE", array_shift($sql)); + + $this->assertEquals(array(), $sql, "SQL Array should be empty now."); + $this->assertEquals(17, $sqlCount, "Total of 17 queries should be executed"); } - + public function testGetCreateSchemaSql2() { $classes = array( @@ -62,11 +66,11 @@ class PostgreSqlSchemaToolTest extends \Doctrine\Tests\OrmFunctionalTestCase $sql = $tool->getCreateSchemaSql($classes); $this->assertEquals(2, count($sql)); - + $this->assertEquals('CREATE TABLE decimal_model (id INT NOT NULL, "decimal" NUMERIC(5, 2) NOT NULL, "high_scale" NUMERIC(14, 4) NOT NULL, PRIMARY KEY(id))', $sql[0]); $this->assertEquals("CREATE SEQUENCE decimal_model_id_seq INCREMENT BY 1 MINVALUE 1 START 1", $sql[1]); } - + public function testGetCreateSchemaSql3() { $classes = array( @@ -75,12 +79,12 @@ class PostgreSqlSchemaToolTest extends \Doctrine\Tests\OrmFunctionalTestCase $tool = new SchemaTool($this->_em); $sql = $tool->getCreateSchemaSql($classes); - + $this->assertEquals(2, count($sql)); $this->assertEquals("CREATE TABLE boolean_model (id INT NOT NULL, booleanField BOOLEAN NOT NULL, PRIMARY KEY(id))", $sql[0]); $this->assertEquals("CREATE SEQUENCE boolean_model_id_seq INCREMENT BY 1 MINVALUE 1 START 1", $sql[1]); } - + public function testGetDropSchemaSql() { $classes = array( @@ -91,8 +95,8 @@ class PostgreSqlSchemaToolTest extends \Doctrine\Tests\OrmFunctionalTestCase $tool = new SchemaTool($this->_em); $sql = $tool->getDropSchemaSQL($classes); - - $this->assertEquals(13, count($sql)); + + $this->assertEquals(14, count($sql)); $dropSequenceSQLs = 0; foreach ($sql AS $stmt) { if (strpos($stmt, "DROP SEQUENCE") === 0) { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1040Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1040Test.php index 21a042187..93dc0eea6 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1040Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1040Test.php @@ -24,7 +24,7 @@ class DDC1040Test extends \Doctrine\Tests\OrmFunctionalTestCase $user->name = "John Galt"; $user->username = "jgalt"; $user->status = "inactive"; - + $article = new CmsArticle(); $article->topic = "This is John Galt speaking!"; $article->text = "Yadda Yadda!"; @@ -44,11 +44,10 @@ class DDC1040Test extends \Doctrine\Tests\OrmFunctionalTestCase ->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"; + $dql = "SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.topic = :topic AND a.user = :author AND a.user = :author"; $farticle = $this->_em->createQuery($dql) ->setParameter('author', $user) ->setParameter('topic', 'This is John Galt speaking!') - ->setParameter('text', 'Yadda Yadda!') ->getSingleResult(); $this->assertSame($article, $farticle); @@ -70,12 +69,11 @@ class DDC1040Test extends \Doctrine\Tests\OrmFunctionalTestCase $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"; + $dql = "SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.topic = ?1 AND a.user = ?2 AND a.user = ?3"; $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); diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1151Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1151Test.php index 589edb048..51a2d4555 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1151Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1151Test.php @@ -8,18 +8,18 @@ require_once __DIR__ . '/../../../TestInit.php'; * @group DDC-1151 */ class DDC1151Test extends \Doctrine\Tests\OrmFunctionalTestCase -{ +{ public function testQuoteForeignKey() { if ($this->_em->getConnection()->getDatabasePlatform()->getName() != 'postgresql') { $this->markTestSkipped("This test is useful for all databases, but designed only for postgresql."); } - + $sql = $this->_schemaTool->getCreateSchemaSql(array( $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1151User'), $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1151Group'), )); - + $this->assertEquals("CREATE TABLE \"User\" (id INT NOT NULL, PRIMARY KEY(id))", $sql[0]); $this->assertEquals("CREATE TABLE ddc1151user_ddc1151group (ddc1151user_id INT NOT NULL, ddc1151group_id INT NOT NULL, PRIMARY KEY(ddc1151user_id, ddc1151group_id))", $sql[1]); $this->assertEquals("CREATE INDEX IDX_88A3259AC5AD08A ON ddc1151user_ddc1151group (ddc1151user_id)", $sql[2]); @@ -27,8 +27,8 @@ class DDC1151Test extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals("CREATE TABLE \"Group\" (id INT NOT NULL, PRIMARY KEY(id))", $sql[4]); $this->assertEquals("CREATE SEQUENCE User_id_seq INCREMENT BY 1 MINVALUE 1 START 1", $sql[5]); $this->assertEquals("CREATE SEQUENCE Group_id_seq INCREMENT BY 1 MINVALUE 1 START 1", $sql[6]); - $this->assertEquals("ALTER TABLE ddc1151user_ddc1151group ADD FOREIGN KEY (ddc1151user_id) REFERENCES \"User\"(id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE", $sql[7]); - $this->assertEquals("ALTER TABLE ddc1151user_ddc1151group ADD FOREIGN KEY (ddc1151group_id) REFERENCES \"Group\"(id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE", $sql[8]); + $this->assertEquals("ALTER TABLE ddc1151user_ddc1151group ADD CONSTRAINT FK_88A3259AC5AD08A FOREIGN KEY (ddc1151user_id) REFERENCES \"User\" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE", $sql[7]); + $this->assertEquals("ALTER TABLE ddc1151user_ddc1151group ADD CONSTRAINT FK_88A32597357E0B1 FOREIGN KEY (ddc1151group_id) REFERENCES \"Group\" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE", $sql[8]); } } @@ -40,7 +40,7 @@ class DDC1151User { /** @Id @Column(type="integer") @GeneratedValue */ public $id; - + /** @ManyToMany(targetEntity="DDC1151Group") */ public $groups; } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC117Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC117Test.php index 7057b765d..9e1b5b074 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC117Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC117Test.php @@ -209,8 +209,15 @@ class DDC117Test extends \Doctrine\Tests\OrmFunctionalTestCase $this->article1->addTranslation('en', 'Bar'); $this->article1->addTranslation('en', 'Baz'); - $this->setExpectedException('Exception'); - $this->_em->flush(); + $exceptionThrown = false; + try { + // exception depending on the underyling Database Driver + $this->_em->flush(); + } catch(\Exception $e) { + $exceptionThrown = true; + } + + $this->assertTrue($exceptionThrown, "The underlying database driver throws an exception."); } /** diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1209Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1209Test.php index 472978bc2..bddbbdf44 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1209Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1209Test.php @@ -106,7 +106,7 @@ class DDC1209_3 { /** * @Id - * @Column(type="datetime") + * @Column(type="datetime", name="somedate") */ private $date; diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1225Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1225Test.php index cec258f37..95dcc2b65 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1225Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1225Test.php @@ -21,10 +21,10 @@ class DDC1225Test extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->getClassMetadata(__NAMESPACE__ . '\\DDC1225_TestEntity2'), )); } catch(\PDOException $e) { - + } } - + public function testIssue() { $qb = $this->_em->createQueryBuilder(); @@ -32,10 +32,10 @@ class DDC1225Test extends \Doctrine\Tests\OrmFunctionalTestCase ->select('te1') ->where('te1.testEntity2 = ?1') ->setParameter(1, 0); - + $this->assertEquals( - 'SELECT t0_.test_entity2_id AS test_entity2_id0 FROM te1 t0_ WHERE t0_.test_entity2_id = ?', - $qb->getQuery()->getSQL() + strtolower('SELECT t0_.test_entity2_id AS test_entity2_id0 FROM te1 t0_ WHERE t0_.test_entity2_id = ?'), + strtolower($qb->getQuery()->getSQL()) ); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1228Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1228Test.php index 34aef78cf..6e14e2111 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1228Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1228Test.php @@ -21,58 +21,58 @@ class DDC1228Test extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->getClassMetadata(__NAMESPACE__ . '\\DDC1228User'), $this->_em->getClassMetadata(__NAMESPACE__ . '\\DDC1228Profile'), )); - } catch(\PDOException $e) { - + } catch(\Exception $e) { + } } - + public function testOneToOnePersist() { $user = new DDC1228User; $profile = new DDC1228Profile(); $profile->name = "Foo"; $user->profile = $profile; - + $this->_em->persist($user); $this->_em->persist($profile); $this->_em->flush(); $this->_em->clear(); - + $user = $this->_em->find(__NAMESPACE__ . '\\DDC1228User', $user->id); - + $this->assertFalse($user->getProfile()->__isInitialized__, "Proxy is not initialized"); $user->getProfile()->setName("Bar"); $this->assertTrue($user->getProfile()->__isInitialized__, "Proxy is not initialized"); - + $this->assertEquals("Bar", $user->getProfile()->getName()); $this->assertEquals(array("id" => 1, "name" => "Foo"), $this->_em->getUnitOfWork()->getOriginalEntityData($user->getProfile())); - + $this->_em->flush(); $this->_em->clear(); - + $user = $this->_em->find(__NAMESPACE__ . '\\DDC1228User', $user->id); $this->assertEquals("Bar", $user->getProfile()->getName()); } - + public function testRefresh() { $user = new DDC1228User; $profile = new DDC1228Profile(); $profile->name = "Foo"; $user->profile = $profile; - + $this->_em->persist($user); $this->_em->persist($profile); $this->_em->flush(); $this->_em->clear(); - + $user = $this->_em->getReference(__NAMESPACE__ . '\\DDC1228User', $user->id); - + $this->_em->refresh($user); $user->name = "Baz"; $this->_em->flush(); $this->_em->clear(); - + $user = $this->_em->find(__NAMESPACE__ . '\\DDC1228User', $user->id); $this->assertEquals("Baz", $user->name); } @@ -88,20 +88,20 @@ class DDC1228User * @var int */ public $id; - + /** - * @column(type="string") + * @Column(type="string") * @var string */ - public $name = ''; - + public $name = 'Bar'; + /** * @OneToOne(targetEntity="DDC1228Profile") * @var Profile */ public $profile; - - public function getProfile() + + public function getProfile() { return $this->profile; } @@ -117,13 +117,13 @@ class DDC1228Profile * @var int */ public $id; - + /** * @column(type="string") * @var string */ public $name; - + public function getName() { return $this->name; diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1238Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1238Test.php index 467577a43..6783928ef 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1238Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1238Test.php @@ -19,49 +19,49 @@ class DDC1238Test extends \Doctrine\Tests\OrmFunctionalTestCase $this->_schemaTool->createSchema(array( $this->_em->getClassMetadata(__NAMESPACE__ . '\\DDC1238User'), )); - } catch(\PDOException $e) { - + } catch(\Exception $e) { + } } - + public function testIssue() { $user = new DDC1238User; $user->setName("test"); - + $this->_em->persist($user); $this->_em->flush(); $this->_em->clear(); - + $userId = $user->getId(); $this->_em->clear(); - + $user = $this->_em->getReference(__NAMESPACE__ . '\\DDC1238User', $userId); $this->_em->clear(); - + $userId2 = $user->getId(); $this->assertEquals($userId, $userId2, "This proxy can still be initialized."); } - + public function testIssueProxyClear() { $user = new DDC1238User; $user->setName("test"); - + $this->_em->persist($user); $this->_em->flush(); $this->_em->clear(); - + // force proxy load, getId() doesn't work anymore $user->getName(); $userId = $user->getId(); $this->_em->clear(); - + $user = $this->_em->getReference(__NAMESPACE__ . '\\DDC1238User', $userId); $this->_em->clear(); - + $user2 = $this->_em->getReference(__NAMESPACE__ . '\\DDC1238User', $userId); - + // force proxy load, getId() doesn't work anymore $user->getName(); $this->assertNull($user->getId(), "Now this is null, we already have a user instance of that type"); @@ -75,18 +75,18 @@ class DDC1238User { /** @Id @GeneratedValue @Column(type="integer") */ private $id; - + /** * @Column * @var string */ private $name; - + public function getId() { return $this->id; } - + public function getName() { return $this->name; diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1335Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1335Test.php index e12ee9ab7..e283b9f1c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1335Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1335Test.php @@ -7,56 +7,56 @@ use DateTime; require_once __DIR__ . '/../../../TestInit.php'; /** - * @group DDC-1135 + * @group DDC-1335 */ -class DDC1135Test extends \Doctrine\Tests\OrmFunctionalTestCase +class DDC1335Test extends \Doctrine\Tests\OrmFunctionalTestCase { protected function setUp() { parent::setUp(); try { $this->_schemaTool->createSchema(array( - $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1135User'), - $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1135Phone'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1335User'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1335Phone'), )); $this->loadFixture(); } catch(\Exception $e) { } } - - + + public function testDql() { - $dql = 'SELECT u FROM ' . __NAMESPACE__ . '\DDC1135User u INDEX BY u.id'; + $dql = 'SELECT u FROM ' . __NAMESPACE__ . '\DDC1335User u INDEX BY u.id'; $query = $this->_em->createQuery($dql); $result = $query->getResult(); - + $this->assertEquals(sizeof($result), 3); $this->assertArrayHasKey(1, $result); $this->assertArrayHasKey(2, $result); $this->assertArrayHasKey(3, $result); - - $dql = 'SELECT u, p FROM '.__NAMESPACE__ . '\DDC1135User u INDEX BY u.email INNER JOIN u.phones p INDEX BY p.id'; + + $dql = 'SELECT u, p FROM '.__NAMESPACE__ . '\DDC1335User u INDEX BY u.email INNER JOIN u.phones p INDEX BY p.id'; $query = $this->_em->createQuery($dql); $result = $query->getResult(); - + $this->assertEquals(sizeof($result), 3); $this->assertArrayHasKey('foo@foo.com', $result); $this->assertArrayHasKey('bar@bar.com', $result); $this->assertArrayHasKey('foobar@foobar.com', $result); - + $this->assertEquals(sizeof($result['foo@foo.com']->phones), 3); $this->assertEquals(sizeof($result['bar@bar.com']->phones), 3); $this->assertEquals(sizeof($result['foobar@foobar.com']->phones), 3); - + $this->assertArrayHasKey(1, $result['foo@foo.com']->phones->toArray()); $this->assertArrayHasKey(2, $result['foo@foo.com']->phones->toArray()); $this->assertArrayHasKey(3, $result['foo@foo.com']->phones->toArray()); - + $this->assertArrayHasKey(4, $result['bar@bar.com']->phones->toArray()); $this->assertArrayHasKey(5, $result['bar@bar.com']->phones->toArray()); $this->assertArrayHasKey(6, $result['bar@bar.com']->phones->toArray()); - + $this->assertArrayHasKey(7, $result['foobar@foobar.com']->phones->toArray()); $this->assertArrayHasKey(8, $result['foobar@foobar.com']->phones->toArray()); $this->assertArrayHasKey(9, $result['foobar@foobar.com']->phones->toArray()); @@ -65,77 +65,77 @@ class DDC1135Test extends \Doctrine\Tests\OrmFunctionalTestCase public function testTicket() { $builder = $this->_em->createQueryBuilder(); - $builder->select('u')->from(__NAMESPACE__ . '\DDC1135User', 'u', 'u.id'); + $builder->select('u')->from(__NAMESPACE__ . '\DDC1335User', 'u', 'u.id'); $dql = $builder->getQuery()->getDQL(); $result = $builder->getQuery()->getResult(); - + $this->assertEquals(sizeof($result), 3); $this->assertArrayHasKey(1, $result); $this->assertArrayHasKey(2, $result); $this->assertArrayHasKey(3, $result); - $this->assertEquals('SELECT u FROM ' . __NAMESPACE__ . '\DDC1135User u INDEX BY u.id', $dql); + $this->assertEquals('SELECT u FROM ' . __NAMESPACE__ . '\DDC1335User u INDEX BY u.id', $dql); } - + public function testIndexByUnique() { $builder = $this->_em->createQueryBuilder(); - $builder->select('u')->from(__NAMESPACE__ . '\DDC1135User', 'u', 'u.email'); + $builder->select('u')->from(__NAMESPACE__ . '\DDC1335User', 'u', 'u.email'); $dql = $builder->getQuery()->getDQL(); $result = $builder->getQuery()->getResult(); - + $this->assertEquals(sizeof($result), 3); $this->assertArrayHasKey('foo@foo.com', $result); $this->assertArrayHasKey('bar@bar.com', $result); $this->assertArrayHasKey('foobar@foobar.com', $result); - $this->assertEquals('SELECT u FROM ' . __NAMESPACE__ . '\DDC1135User u INDEX BY u.email', $dql); + $this->assertEquals('SELECT u FROM ' . __NAMESPACE__ . '\DDC1335User u INDEX BY u.email', $dql); } - + public function testIndexWithJoin() { $builder = $this->_em->createQueryBuilder(); $builder->select('u','p') - ->from(__NAMESPACE__ . '\DDC1135User', 'u', 'u.email') + ->from(__NAMESPACE__ . '\DDC1335User', 'u', 'u.email') ->join('u.phones', 'p', null, null, 'p.id'); - + $dql = $builder->getQuery()->getDQL(); $result = $builder->getQuery()->getResult(); - + $this->assertEquals(sizeof($result), 3); $this->assertArrayHasKey('foo@foo.com', $result); $this->assertArrayHasKey('bar@bar.com', $result); $this->assertArrayHasKey('foobar@foobar.com', $result); - + $this->assertEquals(sizeof($result['foo@foo.com']->phones), 3); $this->assertEquals(sizeof($result['bar@bar.com']->phones), 3); $this->assertEquals(sizeof($result['foobar@foobar.com']->phones), 3); - + $this->assertArrayHasKey(1, $result['foo@foo.com']->phones->toArray()); $this->assertArrayHasKey(2, $result['foo@foo.com']->phones->toArray()); $this->assertArrayHasKey(3, $result['foo@foo.com']->phones->toArray()); - + $this->assertArrayHasKey(4, $result['bar@bar.com']->phones->toArray()); $this->assertArrayHasKey(5, $result['bar@bar.com']->phones->toArray()); $this->assertArrayHasKey(6, $result['bar@bar.com']->phones->toArray()); - + $this->assertArrayHasKey(7, $result['foobar@foobar.com']->phones->toArray()); $this->assertArrayHasKey(8, $result['foobar@foobar.com']->phones->toArray()); $this->assertArrayHasKey(9, $result['foobar@foobar.com']->phones->toArray()); - - $this->assertEquals('SELECT u, p FROM '.__NAMESPACE__ . '\DDC1135User u INDEX BY u.email INNER JOIN u.phones p INDEX BY p.id', $dql); + + $this->assertEquals('SELECT u, p FROM '.__NAMESPACE__ . '\DDC1335User u INDEX BY u.email INNER JOIN u.phones p INDEX BY p.id', $dql); } - + private function loadFixture() { $p1 = array('11 xxxx-xxxx','11 yyyy-yyyy','11 zzzz-zzzz'); $p2 = array('22 xxxx-xxxx','22 yyyy-yyyy','22 zzzz-zzzz'); $p3 = array('33 xxxx-xxxx','33 yyyy-yyyy','33 zzzz-zzzz'); - - $u1 = new DDC1135User("foo@foo.com", "Foo",$p1); - $u2 = new DDC1135User("bar@bar.com", "Bar",$p2); - $u3 = new DDC1135User("foobar@foobar.com", "Foo Bar",$p3); - + + $u1 = new DDC1335User("foo@foo.com", "Foo",$p1); + $u2 = new DDC1335User("bar@bar.com", "Bar",$p2); + $u3 = new DDC1335User("foobar@foobar.com", "Foo Bar",$p3); + $this->_em->persist($u1); $this->_em->persist($u2); $this->_em->persist($u3); @@ -148,7 +148,7 @@ class DDC1135Test extends \Doctrine\Tests\OrmFunctionalTestCase /** * @Entity */ -class DDC1135User +class DDC1335User { /** * @Id @Column(type="integer") @@ -160,25 +160,25 @@ class DDC1135User * @Column(type="string", unique=true) */ public $email; - + /** * @Column(type="string") */ public $name; - + /** - * @OneToMany(targetEntity="DDC1135Phone", mappedBy="user", cascade={"persist", "remove"}) + * @OneToMany(targetEntity="DDC1335Phone", mappedBy="user", cascade={"persist", "remove"}) */ public $phones; - + public function __construct($email, $name, array $numbers = array()) { $this->name = $name; $this->email = $email; $this->phones = new \Doctrine\Common\Collections\ArrayCollection(); - + foreach ($numbers as $number) { - $this->phones->add(new DDC1135Phone($this,$number)); + $this->phones->add(new DDC1335Phone($this,$number)); } } } @@ -186,22 +186,22 @@ class DDC1135User /** * @Entity */ -class DDC1135Phone +class DDC1335Phone { /** * @Id * @Column(name="id", type="integer") - * @GeneratedValue(strategy="AUTO") + * @GeneratedValue */ public $id; /** - * @Column(name="number", type="string", nullable = false) + * @Column(name="numericalValue", type="string", nullable = false) */ - public $number; + public $numericalValue; /** - * @ManyToOne(targetEntity="DDC1135User", inversedBy="phones") + * @ManyToOne(targetEntity="DDC1335User", inversedBy="phones") * @JoinColumn(name="user_id", referencedColumnName="id", nullable = false) */ public $user; @@ -209,6 +209,6 @@ class DDC1135Phone public function __construct($user, $number) { $this->user = $user; - $this->number = $number; + $this->numericalValue = $number; } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1404Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1404Test.php new file mode 100644 index 000000000..49a282772 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1404Test.php @@ -0,0 +1,129 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1404ParentEntity'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1404ChildEntity'), + )); + + $this->loadFixtures(); + + } catch (Exception $exc) { + } + } + + public function testTicket() + { + $repository = $this->_em->getRepository(__NAMESPACE__ . '\DDC1404ChildEntity'); + $queryAll = $repository->createNamedQuery('all'); + $queryFirst = $repository->createNamedQuery('first'); + $querySecond = $repository->createNamedQuery('second'); + + + $this->assertEquals('SELECT p FROM Doctrine\Tests\ORM\Functional\Ticket\DDC1404ChildEntity p', $queryAll->getDQL()); + $this->assertEquals('SELECT p FROM Doctrine\Tests\ORM\Functional\Ticket\DDC1404ChildEntity p WHERE p.id = 1', $queryFirst->getDQL()); + $this->assertEquals('SELECT p FROM Doctrine\Tests\ORM\Functional\Ticket\DDC1404ChildEntity p WHERE p.id = 2', $querySecond->getDQL()); + + + $this->assertEquals(sizeof($queryAll->getResult()), 2); + $this->assertEquals(sizeof($queryFirst->getResult()), 1); + $this->assertEquals(sizeof($querySecond->getResult()), 1); + } + + + public function loadFixtures() + { + $c1 = new DDC1404ChildEntity("ChildEntity 1"); + $c2 = new DDC1404ChildEntity("ChildEntity 2"); + + $this->_em->persist($c1); + $this->_em->persist($c2); + + $this->_em->flush(); + } + +} + +/** + * @MappedSuperclass + * + * @NamedQueries({ + * @NamedQuery(name="all", query="SELECT p FROM __CLASS__ p"), + * @NamedQuery(name="first", query="SELECT p FROM __CLASS__ p WHERE p.id = 1"), + * }) + */ +class DDC1404ParentEntity +{ + + /** + * @Id + * @Column(type="integer") + * @GeneratedValue() + */ + protected $id; + + /** + * @return integer + */ + public function getId() + { + return $this->id; + } + +} + +/** + * @Entity + * + * @NamedQueries({ + * @NamedQuery(name="first", query="SELECT p FROM __CLASS__ p WHERE p.id = 1"), + * @NamedQuery(name="second", query="SELECT p FROM __CLASS__ p WHERE p.id = 2") + * }) + */ +class DDC1404ChildEntity extends DDC1404ParentEntity +{ + + /** + * @column(type="string") + */ + private $name; + + /** + * @param string $name + */ + public function __construct($name) + { + $this->name = $name; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1458Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1458Test.php new file mode 100644 index 000000000..2a0541afd --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1458Test.php @@ -0,0 +1,131 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\TestEntity'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\TestAdditionalEntity') + )); + } + + public function testIssue() + { + $testEntity = new TestEntity(); + $testEntity->setValue(3); + $testEntity->setAdditional(new TestAdditionalEntity()); + $this->_em->persist($testEntity); + $this->_em->flush(); + $this->_em->clear(); + + // So here the value is 3 + $this->assertEquals(3, $testEntity->getValue()); + + $test = $this->_em->getRepository(__NAMESPACE__ . '\TestEntity')->find(1); + + // New value is set + $test->setValue(5); + + // So here the value is 5 + $this->assertEquals(5, $test->getValue()); + + // Get the additional entity + $additional = $test->getAdditional(); + + // Still 5.. + $this->assertEquals(5, $test->getValue()); + + // Force the proxy to load + $additional->getBool(); + + // The value should still be 5 + $this->assertEquals(5, $test->getValue()); + } +} + + +/** + * @Entity + */ +class TestEntity +{ + /** + * @Id + * @Column(type="integer") + * @GeneratedValue(strategy="AUTO") + */ + protected $id; + /** + * @Column(type="integer") + */ + protected $value; + /** + * @OneToOne(targetEntity="TestAdditionalEntity", inversedBy="entity", orphanRemoval=true, cascade={"persist", "remove"}) + */ + protected $additional; + + public function getValue() + { + return $this->value; + } + + public function setValue($value) + { + $this->value = $value; + } + + public function getAdditional() + { + return $this->additional; + } + + public function setAdditional($additional) + { + $this->additional = $additional; + } +} +/** + * @Entity + */ +class TestAdditionalEntity +{ + /** + * @Id + * @Column(type="integer") + * @GeneratedValue(strategy="AUTO") + */ + protected $id; + /** + * @OneToOne(targetEntity="TestEntity", mappedBy="additional") + */ + protected $entity; + /** + * @Column(type="boolean") + */ + protected $bool; + + public function __construct() + { + $this->bool = false; + } + + public function getBool() + { + return $this->bool; + } + + public function setBool($bool) + { + $this->bool = $bool; + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1461Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1461Test.php new file mode 100644 index 000000000..a47571a75 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1461Test.php @@ -0,0 +1,86 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1461TwitterAccount'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1461User') + )); + } catch(\Exception $e) { + + } + } + + public function testChangeDetectionDeferredExplicit() + { + $user = new DDC1461User; + $this->_em->persist($user); + $this->_em->flush(); + + $this->assertEquals(\Doctrine\ORM\UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($user, \Doctrine\ORM\UnitOfWork::STATE_NEW), "Entity should be managed."); + $this->assertEquals(\Doctrine\ORM\UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($user), "Entity should be managed."); + + $acc = new DDC1461TwitterAccount; + $user->twitterAccount = $acc; + + $this->_em->persist($user); + $this->_em->flush(); + + $user = $this->_em->find(get_class($user), $user->id); + $this->assertNotNull($user->twitterAccount); + } +} + +/** + * @Entity + * @ChangeTrackingPolicy("DEFERRED_EXPLICIT") + */ +class DDC1461User +{ + /** + * @Id + * @GeneratedValue(strategy="AUTO") + * @Column(type="integer") + */ + public $id; + + /** + * @OneToOne(targetEntity="DDC1461TwitterAccount", orphanRemoval=true, fetch="EAGER", cascade = {"persist"}, inversedBy="user") + * @var TwitterAccount + */ + public $twitterAccount; +} + +/** + * @Entity + * @ChangeTrackingPolicy("DEFERRED_EXPLICIT") + */ +class DDC1461TwitterAccount +{ + /** + * @Id + * @GeneratedValue(strategy="AUTO") + * @Column(type="integer") + */ + public $id; + + /** + * @OneToOne(targetEntity="DDC1461User", fetch="EAGER") + */ + public $user; +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC331Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC331Test.php index 512b1c9ea..2db32b9b8 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC331Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC331Test.php @@ -25,18 +25,15 @@ class DDC331Test extends \Doctrine\Tests\OrmFunctionalTestCase parent::setUp(); } + /** + * @group DDC-331 + */ public function testSelectFieldOnRootEntity() { - $employee = new CompanyEmployee; - $employee->setName('Roman S. Borschel'); - $employee->setSalary(100000); - $employee->setDepartment('IT'); - - $this->_em->persist($employee); - $this->_em->flush(); - $this->_em->clear(); - $q = $this->_em->createQuery('SELECT e.name FROM Doctrine\Tests\Models\Company\CompanyEmployee e'); - $this->assertEquals('SELECT c0_.name AS name0 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id LEFT JOIN company_managers c2_ ON c1_.id = c2_.id', $q->getSql()); + $this->assertEquals( + strtolower('SELECT c0_.name AS name0 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id LEFT JOIN company_managers c2_ ON c1_.id = c2_.id'), + strtolower($q->getSql()) + ); } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC448Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC448Test.php index f0867f352..4ea830863 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC448Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC448Test.php @@ -18,7 +18,10 @@ class DDC448Test extends \Doctrine\Tests\OrmFunctionalTestCase public function testIssue() { $q = $this->_em->createQuery("select b from ".__NAMESPACE__."\\DDC448SubTable b where b.connectedClassId = ?1"); - $this->assertEquals('SELECT d0_.id AS id0, d0_.discr AS discr1, d0_.connectedClassId AS connectedClassId2 FROM SubTable s1_ INNER JOIN DDC448MainTable d0_ ON s1_.id = d0_.id WHERE d0_.connectedClassId = ?', $q->getSQL()); + $this->assertEquals( + strtolower('SELECT d0_.id AS id0, d0_.discr AS discr1, d0_.connectedClassId AS connectedClassId2 FROM SubTable s1_ INNER JOIN DDC448MainTable d0_ ON s1_.id = d0_.id WHERE d0_.connectedClassId = ?'), + strtolower($q->getSQL()) + ); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC493Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC493Test.php index 25f378255..9e8d58be9 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC493Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC493Test.php @@ -18,7 +18,10 @@ class DDC493Test extends \Doctrine\Tests\OrmFunctionalTestCase public function testIssue() { $q = $this->_em->createQuery("select u, c.data from ".__NAMESPACE__."\\DDC493Distributor u JOIN u.contact c"); - $this->assertEquals('SELECT d0_.id AS id0, d1_.data AS data1, d0_.discr AS discr2, d0_.contact AS contact3 FROM DDC493Distributor d2_ INNER JOIN DDC493Customer d0_ ON d2_.id = d0_.id INNER JOIN DDC493Contact d1_ ON d0_.contact = d1_.id', $q->getSQL()); + $this->assertEquals( + strtolower('SELECT d0_.id AS id0, d1_.data AS data1, d0_.discr AS discr2, d0_.contact AS contact3 FROM DDC493Distributor d2_ INNER JOIN DDC493Customer d0_ ON d2_.id = d0_.id INNER JOIN DDC493Contact d1_ ON d0_.contact = d1_.id'), + strtolower($q->getSQL()) + ); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC513Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC513Test.php index 125f297db..b71d674cc 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC513Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC513Test.php @@ -18,7 +18,10 @@ class DDC513Test extends \Doctrine\Tests\OrmFunctionalTestCase public function testIssue() { $q = $this->_em->createQuery("select u from ".__NAMESPACE__."\\DDC513OfferItem u left join u.price p"); - $this->assertEquals('SELECT d0_.id AS id0, d0_.discr AS discr1, d0_.price AS price2 FROM DDC513OfferItem d1_ INNER JOIN DDC513Item d0_ ON d1_.id = d0_.id LEFT JOIN DDC513Price d2_ ON d0_.price = d2_.id', $q->getSQL()); + $this->assertEquals( + strtolower('SELECT d0_.id AS id0, d0_.discr AS discr1, d0_.price AS price2 FROM DDC513OfferItem d1_ INNER JOIN DDC513Item d0_ ON d1_.id = d0_.id LEFT JOIN DDC513Price d2_ ON d0_.price = d2_.id'), + strtolower($q->getSQL()) + ); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC698Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC698Test.php index 9fbfd01ab..4786cc176 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC698Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC698Test.php @@ -30,7 +30,10 @@ class DDC698Test extends \Doctrine\Tests\OrmFunctionalTestCase $sql = $qb->getQuery()->getSQL(); - $this->assertEquals('SELECT p0_.privilegeID AS privilegeID0, p0_.name AS name1, r1_.roleID AS roleID2, r1_.name AS name3, r1_.shortName AS shortName4 FROM Privileges p0_ LEFT JOIN RolePrivileges r2_ ON p0_.privilegeID = r2_.privilegeID LEFT JOIN Roles r1_ ON r1_.roleID = r2_.roleID', $sql); + $this->assertEquals( + strtolower('SELECT p0_.privilegeID AS privilegeID0, p0_.name AS name1, r1_.roleID AS roleID2, r1_.name AS name3, r1_.shortName AS shortName4 FROM Privileges p0_ LEFT JOIN RolePrivileges r2_ ON p0_.privilegeID = r2_.privilegeID LEFT JOIN Roles r1_ ON r1_.roleID = r2_.roleID'), + strtolower($sql) + ); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC719Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC719Test.php index 82cb67223..6bd18ef98 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC719Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC719Test.php @@ -19,7 +19,10 @@ class DDC719Test extends \Doctrine\Tests\OrmFunctionalTestCase { $q = $this->_em->createQuery('SELECT g, c FROM Doctrine\Tests\ORM\Functional\Ticket\DDC719Group g LEFT JOIN g.children c WHERE g.parents IS EMPTY'); - $this->assertEquals('SELECT g0_.name AS name0, g0_.description AS description1, g0_.id AS id2, g1_.name AS name3, g1_.description AS description4, g1_.id AS id5 FROM groups g0_ LEFT JOIN groups_groups g2_ ON g0_.id = g2_.parent_id LEFT JOIN groups g1_ ON g1_.id = g2_.child_id WHERE (SELECT COUNT(*) FROM groups_groups g3_ WHERE g3_.child_id = g0_.id) = 0', $q->getSQL()); + $this->assertEquals( + strtolower('SELECT g0_.name AS name0, g0_.description AS description1, g0_.id AS id2, g1_.name AS name3, g1_.description AS description4, g1_.id AS id5 FROM groups g0_ LEFT JOIN groups_groups g2_ ON g0_.id = g2_.parent_id LEFT JOIN groups g1_ ON g1_.id = g2_.child_id WHERE (SELECT COUNT(*) FROM groups_groups g3_ WHERE g3_.child_id = g0_.id) = 0'), + strtolower($q->getSQL()) + ); } } @@ -28,12 +31,12 @@ class DDC719Test extends \Doctrine\Tests\OrmFunctionalTestCase */ class Entity { - /** + /** * @Id @GeneratedValue - * @Column(type="integer") + * @Column(type="integer") */ protected $id; - + public function getId() { return $this->id; } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC949Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC949Test.php index 473a8679f..a3734b75a 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC949Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC949Test.php @@ -34,10 +34,10 @@ class DDC949Test extends \Doctrine\Tests\OrmFunctionalTestCase $true = $this->_em->getRepository('Doctrine\Tests\Models\Generic\BooleanModel')->findOneBy(array('booleanField' => true)); $false = $this->_em->getRepository('Doctrine\Tests\Models\Generic\BooleanModel')->findOneBy(array('booleanField' => false)); - $this->assertInstanceOf('Doctrine\Tests\Models\Generic\BooleanModel', $true); + $this->assertInstanceOf('Doctrine\Tests\Models\Generic\BooleanModel', $true, "True model not found"); $this->assertTrue($true->booleanField, "True Boolean Model should be true."); - $this->assertInstanceOf('Doctrine\Tests\Models\Generic\BooleanModel', $false); + $this->assertInstanceOf('Doctrine\Tests\Models\Generic\BooleanModel', $false, "False model not found"); $this->assertFalse($false->booleanField, "False Boolean Model should be false."); } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php index a318f78af..dfd40b9ff 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php @@ -9,13 +9,34 @@ require_once __DIR__ . '/../../TestInit.php'; class ArrayHydratorTest extends HydrationTestCase { + public function provideDataForUserEntityResult() + { + return array( + array(0), + array('user'), + ); + } + + public function provideDataForMultipleRootEntityResult() + { + return array( + array(0, 0), + array('user', 0), + array(0, 'article'), + array('user', 'article'), + ); + } + /** - * Select u.id, u.name from Doctrine\Tests\Models\CMS\CmsUser u + * SELECT PARTIAL u.{id, name} + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * + * @dataProvider provideDataForUserEntityResult */ - public function testSimpleEntityQuery() + public function testSimpleEntityQuery($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); @@ -24,35 +45,39 @@ class ArrayHydratorTest extends HydrationTestCase array( 'u__id' => '1', 'u__name' => 'romanb' - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage' - ) - ); + ) + ); - - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); + $this->assertEquals(1, $result[0]['id']); $this->assertEquals('romanb', $result[0]['name']); + $this->assertEquals(2, $result[1]['id']); $this->assertEquals('jwage', $result[1]['name']); } /** - * + * SELECT PARTIAL u.{id, name}, PARTIAL a.{id, topic} + * FROM Doctrine\Tests\Models\CMS\CmsUser u, Doctrine\Tests\Models\CMS\CmsArticle a + * + * @dataProvider provideDataForMultipleRootEntityResult */ - public function testSimpleMultipleRootEntityQuery() + public function testSimpleMultipleRootEntityQuery($userEntityKey, $articleEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsArticle', 'a'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsArticle', 'a', $articleEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); $rsm->addFieldResult('a', 'a__id', 'id'); @@ -65,47 +90,51 @@ class ArrayHydratorTest extends HydrationTestCase 'u__name' => 'romanb', 'a__id' => '1', 'a__topic' => 'Cool things.' - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'a__id' => '2', 'a__topic' => 'Cool things II.' - ) - ); + ) + ); - - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(4, count($result)); $this->assertEquals(1, $result[0]['id']); $this->assertEquals('romanb', $result[0]['name']); + $this->assertEquals(1, $result[1]['id']); $this->assertEquals('Cool things.', $result[1]['topic']); + $this->assertEquals(2, $result[2]['id']); $this->assertEquals('jwage', $result[2]['name']); + $this->assertEquals(2, $result[3]['id']); $this->assertEquals('Cool things II.', $result[3]['topic']); } /** - * select u.id, u.status, p.phonenumber, upper(u.name) nameUpper from User u - * join u.phonenumbers p - * = - * select u.id, u.status, p.phonenumber, upper(u.name) as u__0 from USERS u - * INNER JOIN PHONENUMBERS p ON u.id = p.user_id + * SELECT PARTIAL u.{id, status}, PARTIAL p.{phonenumber}, UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * JOIN u.phonenumbers p + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryFetchJoin() + public function testMixedQueryFetchJoin($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', 'p', - 'u', 'phonenumbers'); + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' + ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'nameUpper'); @@ -119,54 +148,56 @@ class ArrayHydratorTest extends HydrationTestCase 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '42', - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', 'p__phonenumber' => '91' - ) - ); + ) + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); $this->assertTrue(is_array($result[0])); $this->assertTrue(is_array($result[1])); // first user => 2 phonenumbers - $this->assertEquals(2, count($result[0][0]['phonenumbers'])); + $this->assertEquals(2, count($result[0][$userEntityKey]['phonenumbers'])); $this->assertEquals('ROMANB', $result[0]['nameUpper']); + // second user => 1 phonenumber - $this->assertEquals(1, count($result[1][0]['phonenumbers'])); + $this->assertEquals(1, count($result[1][$userEntityKey]['phonenumbers'])); $this->assertEquals('JWAGE', $result[1]['nameUpper']); - $this->assertEquals(42, $result[0][0]['phonenumbers'][0]['phonenumber']); - $this->assertEquals(43, $result[0][0]['phonenumbers'][1]['phonenumber']); - $this->assertEquals(91, $result[1][0]['phonenumbers'][0]['phonenumber']); + $this->assertEquals(42, $result[0][$userEntityKey]['phonenumbers'][0]['phonenumber']); + $this->assertEquals(43, $result[0][$userEntityKey]['phonenumbers'][1]['phonenumber']); + $this->assertEquals(91, $result[1][$userEntityKey]['phonenumbers'][0]['phonenumber']); } /** - * select u.id, u.status, count(p.phonenumber) numPhones from User u - * join u.phonenumbers p group by u.status, u.id - * = - * select u.id, u.status, count(p.phonenumber) as p__0 from USERS u - * INNER JOIN PHONENUMBERS p ON u.id = p.user_id group by u.id, u.status + * SELECT PARTIAL u.{id, status}, COUNT(p.phonenumber) AS numPhones + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * JOIN u.phonenumbers p + * GROUP BY u.status, u.id + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryNormalJoin() + public function testMixedQueryNormalJoin($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'numPhones'); @@ -178,45 +209,50 @@ class ArrayHydratorTest extends HydrationTestCase 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => '2', - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => '1', - ) - ); + ) + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); $this->assertTrue(is_array($result)); $this->assertTrue(is_array($result[0])); $this->assertTrue(is_array($result[1])); + // first user => 2 phonenumbers + $this->assertArrayHasKey($userEntityKey, $result[0]); $this->assertEquals(2, $result[0]['numPhones']); + // second user => 1 phonenumber + $this->assertArrayHasKey($userEntityKey, $result[1]); $this->assertEquals(1, $result[1]['numPhones']); } /** - * select u.id, u.status, upper(u.name) nameUpper from User u index by u.id - * join u.phonenumbers p indexby p.phonenumber - * = - * select u.id, u.status, upper(u.name) as p__0 from USERS u - * INNER JOIN PHONENUMBERS p ON u.id = p.user_id + * SELECT PARTIAL u.{id, status}, UPPER(u.name) nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * INDEX BY u.id + * JOIN u.phonenumbers p + * INDEX BY p.phonenumber + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryFetchJoinCustomIndex() + public function testMixedQueryFetchJoinCustomIndex($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', - 'p', - 'u', - 'phonenumbers' + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -233,43 +269,46 @@ class ArrayHydratorTest extends HydrationTestCase 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '42', - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', 'p__phonenumber' => '91' - ) - ); + ) + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); - $this->assertTrue(is_array($result[0])); $this->assertTrue(is_array($result[1])); + $this->assertTrue(is_array($result[2])); // test the scalar values - $this->assertEquals('ROMANB', $result[0]['nameUpper']); - $this->assertEquals('JWAGE', $result[1]['nameUpper']); + $this->assertEquals('ROMANB', $result[1]['nameUpper']); + $this->assertEquals('JWAGE', $result[2]['nameUpper']); + // first user => 2 phonenumbers. notice the custom indexing by user id - $this->assertEquals(2, count($result[0]['1']['phonenumbers'])); + $this->assertEquals(2, count($result[1][$userEntityKey]['phonenumbers'])); + // second user => 1 phonenumber. notice the custom indexing by user id - $this->assertEquals(1, count($result[1]['2']['phonenumbers'])); + $this->assertEquals(1, count($result[2][$userEntityKey]['phonenumbers'])); + // test the custom indexing of the phonenumbers - $this->assertTrue(isset($result[0]['1']['phonenumbers']['42'])); - $this->assertTrue(isset($result[0]['1']['phonenumbers']['43'])); - $this->assertTrue(isset($result[1]['2']['phonenumbers']['91'])); + $this->assertTrue(isset($result[1][$userEntityKey]['phonenumbers']['42'])); + $this->assertTrue(isset($result[1][$userEntityKey]['phonenumbers']['43'])); + $this->assertTrue(isset($result[2][$userEntityKey]['phonenumbers']['91'])); } /** @@ -288,16 +327,16 @@ class ArrayHydratorTest extends HydrationTestCase $rsm = new ResultSetMapping; $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', - 'p', - 'u', - 'phonenumbers' + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' ); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsArticle', - 'a', - 'u', - 'articles' + 'Doctrine\Tests\Models\CMS\CmsArticle', + 'a', + 'u', + 'articles' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -316,15 +355,15 @@ class ArrayHydratorTest extends HydrationTestCase 'p__phonenumber' => '42', 'a__id' => '1', 'a__topic' => 'Getting things done!' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', 'a__id' => '1', 'a__topic' => 'Getting things done!' - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', @@ -332,15 +371,15 @@ class ArrayHydratorTest extends HydrationTestCase 'p__phonenumber' => '42', 'a__id' => '2', 'a__topic' => 'ZendCon' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', 'a__id' => '2', 'a__topic' => 'ZendCon' - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', @@ -348,21 +387,20 @@ class ArrayHydratorTest extends HydrationTestCase 'p__phonenumber' => '91', 'a__id' => '3', 'a__topic' => 'LINQ' - ), - array( + ), + array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', 'p__phonenumber' => '91', 'a__id' => '4', 'a__topic' => 'PHP6' - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); $this->assertTrue(is_array($result)); @@ -408,22 +446,22 @@ class ArrayHydratorTest extends HydrationTestCase $rsm = new ResultSetMapping; $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', - 'p', - 'u', - 'phonenumbers' + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' ); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsArticle', - 'a', - 'u', - 'articles' + 'Doctrine\Tests\Models\CMS\CmsArticle', + 'a', + 'u', + 'articles' ); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsComment', - 'c', - 'a', - 'comments' + 'Doctrine\Tests\Models\CMS\CmsComment', + 'c', + 'a', + 'comments' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -446,8 +484,8 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'Getting things done!', 'c__id' => '1', 'c__topic' => 'First!' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', @@ -456,7 +494,7 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'Getting things done!', 'c__id' => '1', 'c__topic' => 'First!' - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', @@ -466,8 +504,8 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'ZendCon', 'c__id' => null, 'c__topic' => null - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', @@ -476,7 +514,7 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'ZendCon', 'c__id' => null, 'c__topic' => null - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', @@ -486,8 +524,8 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'LINQ', 'c__id' => null, 'c__topic' => null - ), - array( + ), + array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', @@ -496,13 +534,12 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'PHP6', 'c__id' => null, 'c__topic' => null - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); $this->assertTrue(is_array($result)); @@ -565,11 +602,12 @@ class ArrayHydratorTest extends HydrationTestCase $rsm = new ResultSetMapping; $rsm->addEntityResult('Doctrine\Tests\Models\Forum\ForumCategory', 'c'); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\Forum\ForumBoard', - 'b', - 'c', - 'boards' + 'Doctrine\Tests\Models\Forum\ForumBoard', + 'b', + 'c', + 'boards' ); + $rsm->addFieldResult('c', 'c__id', 'id'); $rsm->addFieldResult('c', 'c__position', 'position'); $rsm->addFieldResult('c', 'c__name', 'name'); @@ -585,15 +623,15 @@ class ArrayHydratorTest extends HydrationTestCase 'b__id' => '1', 'b__position' => '0', //'b__category_id' => '1' - ), - array( + ), + array( 'c__id' => '2', 'c__position' => '0', 'c__name' => 'Second', 'b__id' => '2', 'b__position' => '0', //'b__category_id' => '2' - ), + ), array( 'c__id' => '1', 'c__position' => '0', @@ -601,21 +639,20 @@ class ArrayHydratorTest extends HydrationTestCase 'b__id' => '3', 'b__position' => '1', //'b__category_id' => '1' - ), - array( + ), + array( 'c__id' => '1', 'c__position' => '0', 'c__name' => 'First', 'b__id' => '4', 'b__position' => '2', //'b__category_id' => '1' - ) - ); + ) + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); $this->assertTrue(is_array($result)); @@ -626,15 +663,19 @@ class ArrayHydratorTest extends HydrationTestCase $this->assertTrue(isset($result[1]['boards'])); $this->assertEquals(1, count($result[1]['boards'])); } - + /** - * DQL: select partial u.{id,status}, a.id, a.topic, c.id as cid, c.topic as ctopic from CmsUser u left join u.articles a left join a.comments c - * + * SELECT PARTIAL u.{id,status}, a.id, a.topic, c.id as cid, c.topic as ctopic + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * LEFT JOIN u.articles a + * LEFT JOIN a.comments c + * + * @dataProvider provideDataForUserEntityResult */ - /*public function testChainedJoinWithScalars() + public function testChainedJoinWithScalars($entityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $entityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('a__id', 'id'); @@ -652,7 +693,7 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'The First', 'c__id' => '1', 'c__topic' => 'First Comment' - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', @@ -660,47 +701,52 @@ class ArrayHydratorTest extends HydrationTestCase 'a__topic' => 'The First', 'c__id' => '2', 'c__topic' => 'Second Comment' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'a__id' => '42', 'a__topic' => 'The Answer', 'c__id' => null, 'c__topic' => null - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); + $result = $hydrator->hydrateAll($stmt, $rsm); - $result = $hydrator->hydrateAll($stmt, $rsm); - $this->assertEquals(3, count($result)); - - $this->assertEquals(2, count($result[0][0])); // User array + + $this->assertEquals(2, count($result[0][$entityKey])); // User array $this->assertEquals(1, $result[0]['id']); $this->assertEquals('The First', $result[0]['topic']); $this->assertEquals(1, $result[0]['cid']); $this->assertEquals('First Comment', $result[0]['ctopic']); - - $this->assertEquals(2, count($result[1][0])); // User array, duplicated + + $this->assertEquals(2, count($result[1][$entityKey])); // User array, duplicated $this->assertEquals(1, $result[1]['id']); // duplicated $this->assertEquals('The First', $result[1]['topic']); // duplicated $this->assertEquals(2, $result[1]['cid']); $this->assertEquals('Second Comment', $result[1]['ctopic']); - - $this->assertEquals(2, count($result[2][0])); // User array, duplicated + + $this->assertEquals(2, count($result[2][$entityKey])); // User array, duplicated $this->assertEquals(42, $result[2]['id']); $this->assertEquals('The Answer', $result[2]['topic']); $this->assertNull($result[2]['cid']); $this->assertNull($result[2]['ctopic']); - }*/ + } - public function testResultIteration() + /** + * SELECT PARTIAL u.{id, status}, UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * + * @dataProvider provideDataForUserEntityResult + */ + public function testResultIteration($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); @@ -709,23 +755,22 @@ class ArrayHydratorTest extends HydrationTestCase array( 'u__id' => '1', 'u__name' => 'romanb' - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage' - ) - ); + ) + ); - - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); + $iterator = $hydrator->iterate($stmt, $rsm); + $rowNum = 0; - $iterableResult = $hydrator->iterate($stmt, $rsm); - - $rowNum = 0; - while (($row = $iterableResult->next()) !== false) { + while (($row = $iterator->next()) !== false) { $this->assertEquals(1, count($row)); $this->assertTrue(is_array($row[0])); + if ($rowNum == 0) { $this->assertEquals(1, $row[0]['id']); $this->assertEquals('romanb', $row[0]['name']); @@ -733,17 +778,22 @@ class ArrayHydratorTest extends HydrationTestCase $this->assertEquals(2, $row[0]['id']); $this->assertEquals('jwage', $row[0]['name']); } + ++$rowNum; } } /** + * SELECT PARTIAL u.{id, name} + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * * @group DDC-644 + * @dataProvider provideDataForUserEntityResult */ - public function testSkipUnknownColumns() + public function testSkipUnknownColumns($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); @@ -753,24 +803,30 @@ class ArrayHydratorTest extends HydrationTestCase 'u__id' => '1', 'u__name' => 'romanb', 'foo' => 'bar', // unknown! - ), + ), ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(1, count($result)); + $this->assertArrayHasKey('id', $result[0]); + $this->assertArrayHasKey('name', $result[0]); + $this->assertArrayNotHasKey('foo', $result[0]); } /** + * SELECT PARTIAL u.{id, status}, UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * * @group DDC-1358 + * @dataProvider provideDataForUserEntityResult */ - public function testMissingIdForRootEntity() + public function testMissingIdForRootEntity($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'nameUpper'); @@ -782,28 +838,27 @@ class ArrayHydratorTest extends HydrationTestCase 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', - ), + ), array( 'u__id' => null, 'u__status' => null, 'sclr0' => 'ROMANB', - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', - ), + ), array( 'u__id' => null, 'u__status' => null, 'sclr0' => 'JWAGE', - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(4, count($result), "Should hydrate four results."); @@ -812,9 +867,54 @@ class ArrayHydratorTest extends HydrationTestCase $this->assertEquals('JWAGE', $result[2]['nameUpper']); $this->assertEquals('JWAGE', $result[3]['nameUpper']); - $this->assertEquals(array('id' => 1, 'status' => 'developer'), $result[0][0]); - $this->assertNull($result[1][0]); - $this->assertEquals(array('id' => 2, 'status' => 'developer'), $result[2][0]); - $this->assertNull($result[3][0]); + $this->assertEquals(array('id' => 1, 'status' => 'developer'), $result[0][$userEntityKey]); + $this->assertNull($result[1][$userEntityKey]); + $this->assertEquals(array('id' => 2, 'status' => 'developer'), $result[2][$userEntityKey]); + $this->assertNull($result[3][$userEntityKey]); + } + + /** + * SELECT PARTIAL u.{id, status}, UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * INDEX BY u.id + * + * @group DDC-1385 + * @dataProvider provideDataForUserEntityResult + */ + public function testIndexByAndMixedResult($userEntityKey) + { + $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); + $rsm->addFieldResult('u', 'u__id', 'id'); + $rsm->addFieldResult('u', 'u__status', 'status'); + $rsm->addScalarResult('sclr0', 'nameUpper'); + $rsm->addIndexBy('u', 'id'); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'sclr0' => 'ROMANB', + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'sclr0' => 'JWAGE', + ), + ); + + $stmt = new HydratorMockStatement($resultSet); + $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); + $result = $hydrator->hydrateAll($stmt, $rsm); + + $this->assertEquals(2, count($result)); + + $this->assertTrue(isset($result[1])); + $this->assertEquals(1, $result[1][$userEntityKey]['id']); + + $this->assertTrue(isset($result[2])); + $this->assertEquals(2, $result[2][$userEntityKey]['id']); } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Hydration/CustomHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/CustomHydratorTest.php index 8daf961b5..09be6bcd3 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/CustomHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/CustomHydratorTest.php @@ -22,7 +22,7 @@ class CustomHydratorTest extends HydrationTestCase class CustomHydrator extends AbstractHydrator { - protected function _hydrateAll() + protected function hydrateAllData() { return $this->_stmt->fetchAll(PDO::FETCH_ASSOC); } diff --git a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php index 581a3504a..c3d6a9412 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php @@ -15,13 +15,42 @@ require_once __DIR__ . '/../../TestInit.php'; class ObjectHydratorTest extends HydrationTestCase { + public function provideDataForUserEntityResult() + { + return array( + array(0), + array('user'), + ); + } + + public function provideDataForMultipleRootEntityResult() + { + return array( + array(0, 0), + array('user', 0), + array(0, 'article'), + array('user', 'article'), + ); + } + + public function provideDataForProductEntityResult() + { + return array( + array(0), + array('product'), + ); + } + /** - * Select u.id, u.name from \Doctrine\Tests\Models\CMS\CmsUser u + * SELECT PARTIAL u.{id,name} + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * + * @dataProvider provideDataForUserEntityResult */ - public function testSimpleEntityQuery() + public function testSimpleEntityScalarFieldsQuery($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); @@ -30,35 +59,40 @@ class ObjectHydratorTest extends HydrationTestCase array( 'u__id' => '1', 'u__name' => 'romanb' - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage' - ) - ); + ) + ); - - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0]); $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1]); + $this->assertEquals(1, $result[0]->id); $this->assertEquals('romanb', $result[0]->name); + $this->assertEquals(2, $result[1]->id); $this->assertEquals('jwage', $result[1]->name); } /** + * SELECT PARTIAL u.{id,name} + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * * @group DDC-644 + * @dataProvider provideDataForUserEntityResult */ - public function testSkipUnknownColumns() + public function testSkipUnknownColumns($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); @@ -68,25 +102,68 @@ class ObjectHydratorTest extends HydrationTestCase 'u__id' => '1', 'u__name' => 'romanb', 'foo' => 'bar', // unknown! - ), + ), ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(1, count($result)); } /** - * Select u.id, u.name from \Doctrine\Tests\Models\CMS\CmsUser u + * SELECT u.id, u.name + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * + * @dataProvider provideDataForUserEntityResult */ - public function testSimpleMultipleRootEntityQuery() + public function testScalarQueryWithoutResultVariables($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsArticle', 'a'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); + $rsm->addScalarResult('sclr0', 'id'); + $rsm->addScalarResult('sclr1', 'name'); + + // Faked result set + $resultSet = array( + array( + 'sclr0' => '1', + 'sclr1' => 'romanb' + ), + array( + 'sclr0' => '2', + 'sclr1' => 'jwage' + ) + ); + + $stmt = new HydratorMockStatement($resultSet); + $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + + $this->assertEquals(2, count($result)); + + $this->assertInternalType('array', $result[0]); + $this->assertInternalType('array', $result[1]); + + $this->assertEquals(1, $result[0]['id']); + $this->assertEquals('romanb', $result[0]['name']); + + $this->assertEquals(2, $result[1]['id']); + $this->assertEquals('jwage', $result[1]['name']); + } + + /** + * SELECT PARTIAL u.{id, name}, PARTIAL a.{id, topic} + * FROM Doctrine\Tests\Models\CMS\CmsUser u, Doctrine\Tests\Models\CMS\CmsArticle a + * + * @dataProvider provideDataForMultipleRootEntityResult + */ + public function testSimpleMultipleRootEntityQuery($userEntityKey, $articleEntityKey) + { + $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsArticle', 'a', $articleEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); $rsm->addFieldResult('a', 'a__id', 'id'); @@ -99,23 +176,21 @@ class ObjectHydratorTest extends HydrationTestCase 'u__name' => 'romanb', 'a__id' => '1', 'a__topic' => 'Cool things.' - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'a__id' => '2', 'a__topic' => 'Cool things II.' - ) - ); + ) + ); - - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(4, count($result)); - + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0]); $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1]); $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[2]); @@ -123,21 +198,27 @@ class ObjectHydratorTest extends HydrationTestCase $this->assertEquals(1, $result[0]->id); $this->assertEquals('romanb', $result[0]->name); + $this->assertEquals(1, $result[1]->id); $this->assertEquals('Cool things.', $result[1]->topic); + $this->assertEquals(2, $result[2]->id); $this->assertEquals('jwage', $result[2]->name); + $this->assertEquals(2, $result[3]->id); $this->assertEquals('Cool things II.', $result[3]->topic); } /** - * Select p from \Doctrine\Tests\Models\ECommerce\ECommerceProduct p + * SELECT p + * FROM Doctrine\Tests\Models\ECommerce\ECommerceProduct p + * + * @dataProvider provideDataForProductEntityResult */ - public function testCreatesProxyForLazyLoadingWithForeignKeys() + public function testCreatesProxyForLazyLoadingWithForeignKeys($productEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\ECommerce\ECommerceProduct', 'p'); + $rsm->addEntityResult('Doctrine\Tests\Models\ECommerce\ECommerceProduct', 'p', $productEntityKey ?: null); $rsm->addFieldResult('p', 'p__id', 'id'); $rsm->addFieldResult('p', 'p__name', 'name'); $rsm->addMetaResult('p', 'p__shipping_id', 'shipping_id'); @@ -148,8 +229,8 @@ class ObjectHydratorTest extends HydrationTestCase 'p__id' => '1', 'p__name' => 'Doctrine Book', 'p__shipping_id' => 42 - ) - ); + ) + ); $proxyInstance = new \Doctrine\Tests\Models\ECommerce\ECommerceShipping(); @@ -157,8 +238,7 @@ class ObjectHydratorTest extends HydrationTestCase $proxyFactory = $this->getMock('Doctrine\ORM\Proxy\ProxyFactory', array('getProxy'), array(), '', false, false, false); $proxyFactory->expects($this->once()) ->method('getProxy') - ->with($this->equalTo('Doctrine\Tests\Models\ECommerce\ECommerceShipping'), - array('id' => 42)) + ->with($this->equalTo('Doctrine\Tests\Models\ECommerce\ECommerceShipping'), array('id' => 42)) ->will($this->returnValue($proxyInstance)); $this->_em->setProxyFactory($proxyFactory); @@ -167,36 +247,36 @@ class ObjectHydratorTest extends HydrationTestCase $metadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\ECommerce\ECommerceProduct'); $metadata->associationMappings['shipping']['fetch'] = ClassMetadata::FETCH_LAZY; - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm); + $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(1, count($result)); + $this->assertInstanceOf('Doctrine\Tests\Models\ECommerce\ECommerceProduct', $result[0]); } /** - * select u.id, u.status, p.phonenumber, upper(u.name) nameUpper from User u - * join u.phonenumbers p - * = - * select u.id, u.status, p.phonenumber, upper(u.name) as u__0 from USERS u - * INNER JOIN PHONENUMBERS p ON u.id = p.user_id + * SELECT PARTIAL u.{id, status}, PARTIAL p.{phonenumber}, UPPER(u.name) nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * JOIN u.phonenumbers p + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryFetchJoin() + public function testMixedQueryFetchJoin($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', - 'p', - 'u', - 'phonenumbers' + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); - $rsm->addScalarResult('sclr0', 'nameUpper'); $rsm->addFieldResult('p', 'p__phonenumber', 'phonenumber'); + $rsm->addScalarResult('sclr0', 'nameUpper'); // Faked result set $resultSet = array( @@ -204,63 +284,66 @@ class ObjectHydratorTest extends HydrationTestCase array( 'u__id' => '1', 'u__status' => 'developer', - 'sclr0' => 'ROMANB', 'p__phonenumber' => '42', - ), + 'sclr0' => 'ROMANB', + ), array( 'u__id' => '1', 'u__status' => 'developer', - 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', - ), + 'sclr0' => 'ROMANB', + ), array( 'u__id' => '2', 'u__status' => 'developer', + 'p__phonenumber' => '91', 'sclr0' => 'JWAGE', - 'p__phonenumber' => '91' - ) - ); + ) + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); - $this->assertTrue(is_array($result)); - $this->assertTrue(is_array($result[0])); - $this->assertTrue(is_array($result[1])); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->phonenumbers); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][0]->phonenumbers[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][0]->phonenumbers[1]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][0]); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][0]->phonenumbers); + $this->assertInternalType('array', $result); + $this->assertInternalType('array', $result[0]); + $this->assertInternalType('array', $result[1]); + + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][$userEntityKey]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][$userEntityKey]->phonenumbers); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][$userEntityKey]->phonenumbers[0]); + + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][$userEntityKey]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][$userEntityKey]->phonenumbers); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][$userEntityKey]->phonenumbers[1]); // first user => 2 phonenumbers - $this->assertEquals(2, count($result[0][0]->phonenumbers)); + $this->assertEquals(2, count($result[0][$userEntityKey]->phonenumbers)); $this->assertEquals('ROMANB', $result[0]['nameUpper']); + // second user => 1 phonenumber - $this->assertEquals(1, count($result[1][0]->phonenumbers)); + $this->assertEquals(1, count($result[1][$userEntityKey]->phonenumbers)); $this->assertEquals('JWAGE', $result[1]['nameUpper']); - $this->assertEquals(42, $result[0][0]->phonenumbers[0]->phonenumber); - $this->assertEquals(43, $result[0][0]->phonenumbers[1]->phonenumber); - $this->assertEquals(91, $result[1][0]->phonenumbers[0]->phonenumber); + $this->assertEquals(42, $result[0][$userEntityKey]->phonenumbers[0]->phonenumber); + $this->assertEquals(43, $result[0][$userEntityKey]->phonenumbers[1]->phonenumber); + $this->assertEquals(91, $result[1][$userEntityKey]->phonenumbers[0]->phonenumber); } /** - * select u.id, u.status, count(p.phonenumber) numPhones from User u - * join u.phonenumbers p group by u.status, u.id - * = - * select u.id, u.status, count(p.phonenumber) as p__0 from USERS u - * INNER JOIN PHONENUMBERS p ON u.id = p.user_id group by u.id, u.status + * SELECT PARTIAL u.{id, status}, COUNT(p.phonenumber) numPhones + * FROM User u + * JOIN u.phonenumbers p + * GROUP BY u.id + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryNormalJoin() + public function testMixedQueryNormalJoin($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'numPhones'); @@ -272,47 +355,51 @@ class ObjectHydratorTest extends HydrationTestCase 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => '2', - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => '1', - ) - ); + ) + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); - $this->assertTrue(is_array($result)); - $this->assertTrue(is_array($result[0])); - $this->assertTrue(is_array($result[1])); + + $this->assertInternalType('array', $result); + $this->assertInternalType('array', $result[0]); + $this->assertInternalType('array', $result[1]); + // first user => 2 phonenumbers $this->assertEquals(2, $result[0]['numPhones']); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][$userEntityKey]); + // second user => 1 phonenumber $this->assertEquals(1, $result[1]['numPhones']); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][$userEntityKey]); } /** - * select u.id, u.status, upper(u.name) nameUpper from User u index by u.id - * join u.phonenumbers p indexby p.phonenumber - * = - * select u.id, u.status, upper(u.name) as p__0 from USERS u - * INNER JOIN PHONENUMBERS p ON u.id = p.user_id + * SELECT u, p, UPPER(u.name) nameUpper + * FROM User u + * INDEX BY u.id + * JOIN u.phonenumbers p + * INDEX BY p.phonenumber + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryFetchJoinCustomIndex() + public function testMixedQueryFetchJoinCustomIndex($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', - 'p', - 'u', - 'phonenumbers' + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -329,75 +416,75 @@ class ObjectHydratorTest extends HydrationTestCase 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '42', - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', 'p__phonenumber' => '91' - ) - ); + ) + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); - $this->assertTrue(is_array($result)); - $this->assertTrue(is_array($result[0])); - $this->assertTrue(is_array($result[1])); + + $this->assertInternalType('array', $result); + $this->assertInternalType('array', $result[1]); + $this->assertInternalType('array', $result[2]); // test the scalar values - $this->assertEquals('ROMANB', $result[0]['nameUpper']); - $this->assertEquals('JWAGE', $result[1]['nameUpper']); + $this->assertEquals('ROMANB', $result[1]['nameUpper']); + $this->assertEquals('JWAGE', $result[2]['nameUpper']); + + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][$userEntityKey]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[2][$userEntityKey]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][$userEntityKey]->phonenumbers); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0]['1']); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1]['2']); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0]['1']->phonenumbers); // first user => 2 phonenumbers. notice the custom indexing by user id - $this->assertEquals(2, count($result[0]['1']->phonenumbers)); + $this->assertEquals(2, count($result[1][$userEntityKey]->phonenumbers)); + // second user => 1 phonenumber. notice the custom indexing by user id - $this->assertEquals(1, count($result[1]['2']->phonenumbers)); + $this->assertEquals(1, count($result[2][$userEntityKey]->phonenumbers)); + // test the custom indexing of the phonenumbers - $this->assertTrue(isset($result[0]['1']->phonenumbers['42'])); - $this->assertTrue(isset($result[0]['1']->phonenumbers['43'])); - $this->assertTrue(isset($result[1]['2']->phonenumbers['91'])); + $this->assertTrue(isset($result[1][$userEntityKey]->phonenumbers['42'])); + $this->assertTrue(isset($result[1][$userEntityKey]->phonenumbers['43'])); + $this->assertTrue(isset($result[2][$userEntityKey]->phonenumbers['91'])); } /** - * select u.id, u.status, p.phonenumber, upper(u.name) nameUpper, a.id, a.topic - * from User u - * join u.phonenumbers p - * join u.articles a - * = - * select u.id, u.status, p.phonenumber, upper(u.name) as u__0, a.id, a.topic - * from USERS u - * inner join PHONENUMBERS p ON u.id = p.user_id - * inner join ARTICLES a ON u.id = a.user_id + * SELECT u, p, UPPER(u.name) nameUpper, a + * FROM User u + * JOIN u.phonenumbers p + * JOIN u.articles a + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryMultipleFetchJoin() + public function testMixedQueryMultipleFetchJoin($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', - 'p', - 'u', - 'phonenumbers' + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' ); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsArticle', - 'a', - 'u', - 'articles' + 'Doctrine\Tests\Models\CMS\CmsArticle', + 'a', + 'u', + 'articles' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -416,15 +503,15 @@ class ObjectHydratorTest extends HydrationTestCase 'p__phonenumber' => '42', 'a__id' => '1', 'a__topic' => 'Getting things done!' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', 'a__id' => '1', 'a__topic' => 'Getting things done!' - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', @@ -432,15 +519,15 @@ class ObjectHydratorTest extends HydrationTestCase 'p__phonenumber' => '42', 'a__id' => '2', 'a__topic' => 'ZendCon' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', 'a__id' => '2', 'a__topic' => 'ZendCon' - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', @@ -448,77 +535,72 @@ class ObjectHydratorTest extends HydrationTestCase 'p__phonenumber' => '91', 'a__id' => '3', 'a__topic' => 'LINQ' - ), - array( + ), + array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', 'p__phonenumber' => '91', 'a__id' => '4', 'a__topic' => 'PHP6' - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); $this->assertTrue(is_array($result[0])); $this->assertTrue(is_array($result[1])); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->phonenumbers); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][0]->phonenumbers[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][0]->phonenumbers[1]); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->articles); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][0]->articles[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][0]->articles[1]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][0]); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][0]->phonenumbers); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[1][0]->phonenumbers[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][0]->articles[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][0]->articles[1]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][$userEntityKey]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][$userEntityKey]->phonenumbers); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][$userEntityKey]->phonenumbers[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][$userEntityKey]->phonenumbers[1]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][$userEntityKey]->articles); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][$userEntityKey]->articles[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][$userEntityKey]->articles[1]); + + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][$userEntityKey]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][$userEntityKey]->phonenumbers); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[1][$userEntityKey]->phonenumbers[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][$userEntityKey]->articles[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][$userEntityKey]->articles[1]); } /** - * select u.id, u.status, p.phonenumber, upper(u.name) nameUpper, a.id, a.topic, - * c.id, c.topic - * from User u - * join u.phonenumbers p - * join u.articles a - * left join a.comments c - * = - * select u.id, u.status, p.phonenumber, upper(u.name) as u__0, a.id, a.topic, - * c.id, c.topic - * from USERS u - * inner join PHONENUMBERS p ON u.id = p.user_id - * inner join ARTICLES a ON u.id = a.user_id - * left outer join COMMENTS c ON a.id = c.article_id + * SELECT u, p, UPPER(u.name) nameUpper, a, c + * FROM User u + * JOIN u.phonenumbers p + * JOIN u.articles a + * LEFT JOIN a.comments c + * + * @dataProvider provideDataForUserEntityResult */ - public function testMixedQueryMultipleDeepMixedFetchJoin() + public function testMixedQueryMultipleDeepMixedFetchJoin($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', - 'p', - 'u', - 'phonenumbers' + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' ); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsArticle', - 'a', - 'u', - 'articles' + 'Doctrine\Tests\Models\CMS\CmsArticle', + 'a', + 'u', + 'articles' ); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsComment', - 'c', - 'a', - 'comments' + 'Doctrine\Tests\Models\CMS\CmsComment', + 'c', + 'a', + 'comments' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -541,8 +623,8 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'Getting things done!', 'c__id' => '1', 'c__topic' => 'First!' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', @@ -551,7 +633,7 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'Getting things done!', 'c__id' => '1', 'c__topic' => 'First!' - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', @@ -561,8 +643,8 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'ZendCon', 'c__id' => null, 'c__topic' => null - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', @@ -571,7 +653,7 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'ZendCon', 'c__id' => null, 'c__topic' => null - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', @@ -581,8 +663,8 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'LINQ', 'c__id' => null, 'c__topic' => null - ), - array( + ), + array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', @@ -591,43 +673,50 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'PHP6', 'c__id' => null, 'c__topic' => null - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); + $this->assertTrue(is_array($result)); $this->assertTrue(is_array($result[0])); $this->assertTrue(is_array($result[1])); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][$userEntityKey]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][$userEntityKey]); + // phonenumbers - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->phonenumbers); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][0]->phonenumbers[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][0]->phonenumbers[1]); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][0]->phonenumbers); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[1][0]->phonenumbers[0]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][$userEntityKey]->phonenumbers); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][$userEntityKey]->phonenumbers[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[0][$userEntityKey]->phonenumbers[1]); + + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][$userEntityKey]->phonenumbers); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $result[1][$userEntityKey]->phonenumbers[0]); + // articles - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->articles); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][0]->articles[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][0]->articles[1]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][0]->articles[0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][0]->articles[1]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][$userEntityKey]->articles); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][$userEntityKey]->articles[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[0][$userEntityKey]->articles[1]); + + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][$userEntityKey]->articles[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsArticle', $result[1][$userEntityKey]->articles[1]); + // article comments - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->articles[0]->comments); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsComment', $result[0][0]->articles[0]->comments[0]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][$userEntityKey]->articles[0]->comments); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsComment', $result[0][$userEntityKey]->articles[0]->comments[0]); + // empty comment collections - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->articles[1]->comments); - $this->assertEquals(0, count($result[0][0]->articles[1]->comments)); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][0]->articles[0]->comments); - $this->assertEquals(0, count($result[1][0]->articles[0]->comments)); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][0]->articles[1]->comments); - $this->assertEquals(0, count($result[1][0]->articles[1]->comments)); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][$userEntityKey]->articles[1]->comments); + $this->assertEquals(0, count($result[0][$userEntityKey]->articles[1]->comments)); + + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][$userEntityKey]->articles[0]->comments); + $this->assertEquals(0, count($result[1][$userEntityKey]->articles[0]->comments)); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][$userEntityKey]->articles[1]->comments); + $this->assertEquals(0, count($result[1][$userEntityKey]->articles[1]->comments)); } /** @@ -653,10 +742,10 @@ class ObjectHydratorTest extends HydrationTestCase $rsm = new ResultSetMapping; $rsm->addEntityResult('Doctrine\Tests\Models\Forum\ForumCategory', 'c'); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\Forum\ForumBoard', - 'b', - 'c', - 'boards' + 'Doctrine\Tests\Models\Forum\ForumBoard', + 'b', + 'c', + 'boards' ); $rsm->addFieldResult('c', 'c__id', 'id'); $rsm->addFieldResult('c', 'c__position', 'position'); @@ -673,15 +762,15 @@ class ObjectHydratorTest extends HydrationTestCase 'b__id' => '1', 'b__position' => '0', //'b__category_id' => '1' - ), - array( + ), + array( 'c__id' => '2', 'c__position' => '0', 'c__name' => 'Second', 'b__id' => '2', 'b__position' => '0', //'b__category_id' => '2' - ), + ), array( 'c__id' => '1', 'c__position' => '0', @@ -689,50 +778,62 @@ class ObjectHydratorTest extends HydrationTestCase 'b__id' => '3', 'b__position' => '1', //'b__category_id' => '1' - ), - array( + ), + array( 'c__id' => '1', 'c__position' => '0', 'c__name' => 'First', 'b__id' => '4', 'b__position' => '2', //'b__category_id' => '1' - ) - ); + ) + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); + $this->assertInstanceOf('Doctrine\Tests\Models\Forum\ForumCategory', $result[0]); $this->assertInstanceOf('Doctrine\Tests\Models\Forum\ForumCategory', $result[1]); + $this->assertTrue($result[0] !== $result[1]); + $this->assertEquals(1, $result[0]->getId()); $this->assertEquals(2, $result[1]->getId()); + $this->assertTrue(isset($result[0]->boards)); $this->assertEquals(3, count($result[0]->boards)); + $this->assertTrue(isset($result[1]->boards)); $this->assertEquals(1, count($result[1]->boards)); } - - public function testChainedJoinWithEmptyCollections() + + /** + * SELECT PARTIAL u.{id, status}, PARTIAL a.{id, topic}, PARTIAL c.{id, topic} + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * LEFT JOIN u.articles a + * LEFT JOIN a.comments c + * + * @dataProvider provideDataForUserEntityResult + */ + public function testChainedJoinWithEmptyCollections($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsArticle', - 'a', - 'u', - 'articles' + 'Doctrine\Tests\Models\CMS\CmsArticle', + 'a', + 'u', + 'articles' ); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsComment', - 'c', - 'a', - 'comments' + 'Doctrine\Tests\Models\CMS\CmsComment', + 'c', + 'a', + 'comments' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -751,38 +852,43 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => null, 'c__id' => null, 'c__topic' => null - ), - array( + ), + array( 'u__id' => '2', 'u__status' => 'developer', 'a__id' => null, 'a__topic' => null, 'c__id' => null, 'c__topic' => null - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0]); $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1]); + $this->assertEquals(0, $result[0]->articles->count()); $this->assertEquals(0, $result[1]->articles->count()); } - + /** - * DQL: select partial u.{id,status}, a.id, a.topic, c.id as cid, c.topic as ctopic from CmsUser u left join u.articles a left join a.comments c - * + * SELECT PARTIAL u.{id,status}, a.id, a.topic, c.id as cid, c.topic as ctopic + * FROM CmsUser u + * LEFT JOIN u.articles a + * LEFT JOIN a.comments c + * * @group bubu + * @dataProvider provideDataForUserEntityResult */ - /*public function testChainedJoinWithScalars() + /*public function testChainedJoinWithScalars($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('a__id', 'id'); @@ -800,7 +906,7 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'The First', 'c__id' => '1', 'c__topic' => 'First Comment' - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', @@ -808,51 +914,39 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'The First', 'c__id' => '2', 'c__topic' => 'Second Comment' - ), - array( + ), + array( 'u__id' => '1', 'u__status' => 'developer', 'a__id' => '42', 'a__topic' => 'The Answer', 'c__id' => null, 'c__topic' => null - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + \Doctrine\Common\Util\Debug::dump($result, 3); - $this->assertEquals(3, count($result)); - - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); // User object - $this->assertEquals(1, $result[0]['id']); + $this->assertEquals(1, count($result)); + + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][$userEntityKey]); // User object + $this->assertEquals(42, $result[0]['id']); $this->assertEquals('The First', $result[0]['topic']); $this->assertEquals(1, $result[0]['cid']); $this->assertEquals('First Comment', $result[0]['ctopic']); - - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][0]); // Same User object - $this->assertEquals(1, $result[1]['id']); // duplicated - $this->assertEquals('The First', $result[1]['topic']); // duplicated - $this->assertEquals(2, $result[1]['cid']); - $this->assertEquals('Second Comment', $result[1]['ctopic']); - - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[2][0]); // Same User object - $this->assertEquals(42, $result[2]['id']); - $this->assertEquals('The Answer', $result[2]['topic']); - $this->assertNull($result[2]['cid']); - $this->assertNull($result[2]['ctopic']); - - $this->assertTrue($result[0][0] === $result[1][0]); - $this->assertTrue($result[1][0] === $result[2][0]); - $this->assertTrue($result[0][0] === $result[2][0]); }*/ - public function testResultIteration() + /** + * @dataProvider provideDataForUserEntityResult + */ + public function testResultIteration($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); @@ -861,23 +955,22 @@ class ObjectHydratorTest extends HydrationTestCase array( 'u__id' => '1', 'u__name' => 'romanb' - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage' - ) - ); - - - $stmt = new HydratorMockStatement($resultSet); - $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); + ) + ); + $stmt = new HydratorMockStatement($resultSet); + $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); $iterableResult = $hydrator->iterate($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $rowNum = 0; - $rowNum = 0; while (($row = $iterableResult->next()) !== false) { $this->assertEquals(1, count($row)); $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $row[0]); + if ($rowNum == 0) { $this->assertEquals(1, $row[0]->id); $this->assertEquals('romanb', $row[0]->name); @@ -885,21 +978,24 @@ class ObjectHydratorTest extends HydrationTestCase $this->assertEquals(2, $row[0]->id); $this->assertEquals('jwage', $row[0]->name); } + ++$rowNum; } } /** - * This issue tests if, with multiple joined multiple-valued collections the hydration is done correctly. + * Checks if multiple joined multiple-valued collections is hydrated correctly. * - * User x Phonenumbers x Groups blow up the resultset quite a bit, however the hydration should correctly assemble those. + * SELECT PARTIAL u.{id, status}, PARTIAL g.{id, name}, PARTIAL p.{phonenumber} + * FROM Doctrine\Tests\Models\CMS\CmsUser u * * @group DDC-809 + * @dataProvider provideDataForUserEntityResult */ - public function testManyToManyHydration() + public function testManyToManyHydration($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__name', 'name'); $rsm->addJoinedEntityResult('Doctrine\Tests\Models\CMS\CmsGroup', 'g', 'u', 'groups'); @@ -916,106 +1012,112 @@ class ObjectHydratorTest extends HydrationTestCase 'g__id' => '3', 'g__name' => 'TestGroupB', 'p__phonenumber' => 1111, - ), + ), array( 'u__id' => '1', 'u__name' => 'romanb', 'g__id' => '5', 'g__name' => 'TestGroupD', 'p__phonenumber' => 1111, - ), + ), array( 'u__id' => '1', 'u__name' => 'romanb', 'g__id' => '3', 'g__name' => 'TestGroupB', 'p__phonenumber' => 2222, - ), + ), array( 'u__id' => '1', 'u__name' => 'romanb', 'g__id' => '5', 'g__name' => 'TestGroupD', 'p__phonenumber' => 2222, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '2', 'g__name' => 'TestGroupA', 'p__phonenumber' => 3333, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '3', 'g__name' => 'TestGroupB', 'p__phonenumber' => 3333, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '4', 'g__name' => 'TestGroupC', 'p__phonenumber' => 3333, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '5', 'g__name' => 'TestGroupD', 'p__phonenumber' => 3333, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '2', 'g__name' => 'TestGroupA', 'p__phonenumber' => 4444, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '3', 'g__name' => 'TestGroupB', 'p__phonenumber' => 4444, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '4', 'g__name' => 'TestGroupC', 'p__phonenumber' => 4444, - ), + ), array( 'u__id' => '2', 'u__name' => 'jwage', 'g__id' => '5', 'g__name' => 'TestGroupD', 'p__phonenumber' => 4444, - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); + $this->assertContainsOnly('Doctrine\Tests\Models\CMS\CmsUser', $result); + $this->assertEquals(2, count($result[0]->groups)); $this->assertEquals(2, count($result[0]->phonenumbers)); + $this->assertEquals(4, count($result[1]->groups)); $this->assertEquals(2, count($result[1]->phonenumbers)); } /** + * SELECT PARTIAL u.{id, status}, UPPER(u.name) as nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * * @group DDC-1358 + * @dataProvider provideDataForUserEntityResult */ - public function testMissingIdForRootEntity() + public function testMissingIdForRootEntity($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'nameUpper'); @@ -1027,28 +1129,27 @@ class ObjectHydratorTest extends HydrationTestCase 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', - ), + ), array( 'u__id' => null, 'u__status' => null, 'sclr0' => 'ROMANB', - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', - ), + ), array( 'u__id' => null, 'u__status' => null, 'sclr0' => 'JWAGE', - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(4, count($result), "Should hydrate four results."); @@ -1057,25 +1158,30 @@ class ObjectHydratorTest extends HydrationTestCase $this->assertEquals('JWAGE', $result[2]['nameUpper']); $this->assertEquals('JWAGE', $result[3]['nameUpper']); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); - $this->assertNull($result[1][0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[2][0]); - $this->assertNull($result[3][0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][$userEntityKey]); + $this->assertNull($result[1][$userEntityKey]); + + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[2][$userEntityKey]); + $this->assertNull($result[3][$userEntityKey]); } /** + * SELECT PARTIAL u.{id, status}, PARTIAL p.{phonenumber}, UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * LEFT JOIN u.phonenumbers u + * * @group DDC-1358 - * @return void + * @dataProvider provideDataForUserEntityResult */ - public function testMissingIdForCollectionValuedChildEntity() + public function testMissingIdForCollectionValuedChildEntity($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsPhonenumber', - 'p', - 'u', - 'phonenumbers' + 'Doctrine\Tests\Models\CMS\CmsPhonenumber', + 'p', + 'u', + 'phonenumbers' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -1090,49 +1196,54 @@ class ObjectHydratorTest extends HydrationTestCase 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '42', - ), + ), array( 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => null - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', 'p__phonenumber' => '91' - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', 'p__phonenumber' => null - ) - ); + ) + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); - $this->assertEquals(1, $result[0][0]->phonenumbers->count()); - $this->assertEquals(1, $result[1][0]->phonenumbers->count()); + + $this->assertEquals(1, $result[0][$userEntityKey]->phonenumbers->count()); + $this->assertEquals(1, $result[1][$userEntityKey]->phonenumbers->count()); } /** + * SELECT PARTIAL u.{id, status}, PARTIAL a.{id, city}, UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * JOIN u.address a + * * @group DDC-1358 + * @dataProvider provideDataForUserEntityResult */ - public function testMissingIdForSingleValuedChildEntity() + public function testMissingIdForSingleValuedChildEntity($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addJoinedEntityResult( - 'Doctrine\Tests\Models\CMS\CmsAddress', - 'a', - 'u', - 'address' + 'Doctrine\Tests\Models\CMS\CmsAddress', + 'a', + 'u', + 'address' ); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -1150,23 +1261,106 @@ class ObjectHydratorTest extends HydrationTestCase 'sclr0' => 'ROMANB', 'a__id' => 1, 'a__city' => 'Berlin', - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'BENJAMIN', 'a__id' => null, 'a__city' => null, - ), - ); + ), + ); - $stmt = new HydratorMockStatement($resultSet); + $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); - - $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsAddress', $result[0][0]->address); - $this->assertNull($result[1][0]->address); + + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsAddress', $result[0][$userEntityKey]->address); + $this->assertNull($result[1][$userEntityKey]->address); + } + + /** + * SELECT PARTIAL u.{id, status}, UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * INDEX BY u.id + * + * @group DDC-1385 + * @dataProvider provideDataForUserEntityResult + */ + public function testIndexByAndMixedResult($userEntityKey) + { + $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); + $rsm->addFieldResult('u', 'u__id', 'id'); + $rsm->addFieldResult('u', 'u__status', 'status'); + $rsm->addScalarResult('sclr0', 'nameUpper'); + $rsm->addIndexBy('u', 'id'); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'sclr0' => 'ROMANB', + ), + array( + 'u__id' => '2', + 'u__status' => 'developer', + 'sclr0' => 'JWAGE', + ), + ); + + $stmt = new HydratorMockStatement($resultSet); + $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + + $this->assertEquals(2, count($result)); + + $this->assertTrue(isset($result[1])); + $this->assertEquals(1, $result[1][$userEntityKey]->id); + + $this->assertTrue(isset($result[2])); + $this->assertEquals(2, $result[2][$userEntityKey]->id); + } + + /** + * SELECT UPPER(u.name) AS nameUpper + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * + * @group DDC-1385 + * @dataProvider provideDataForUserEntityResult + */ + public function testIndexByScalarsOnly($userEntityKey) + { + $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); + $rsm->addScalarResult('sclr0', 'nameUpper'); + $rsm->addIndexByScalar('sclr0'); + + // Faked result set + $resultSet = array( + //row1 + array( + 'sclr0' => 'ROMANB', + ), + array( + 'sclr0' => 'JWAGE', + ), + ); + + $stmt = new HydratorMockStatement($resultSet); + $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + + $this->assertEquals( + array( + 'ROMANB' => array('nameUpper' => 'ROMANB'), + 'JWAGE' => array('nameUpper' => 'JWAGE') + ), + $result + ); } } diff --git a/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php index 98c43a5b9..c95af376e 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php @@ -327,6 +327,51 @@ abstract class AbstractMappingDriverTest extends \Doctrine\Tests\OrmTestCase $em->getRepository("Doctrine\Tests\Models\DDC869\DDC869ChequePayment")); $this->assertTrue($em->getRepository("Doctrine\Tests\Models\DDC869\DDC869ChequePayment")->isTrue()); } + + /** + * @group DDC-1476 + */ + public function testDefaultFieldType() + { + $driver = $this->_loadDriver(); + $em = $this->_getTestEntityManager(); + $factory = new \Doctrine\ORM\Mapping\ClassMetadataFactory(); + + $em->getConfiguration()->setMetadataDriverImpl($driver); + $factory->setEntityManager($em); + + + $class = $factory->getMetadataFor('Doctrine\Tests\Models\DDC1476\DDC1476EntityWithDefaultFieldType'); + + + $this->assertArrayHasKey('id', $class->fieldMappings); + $this->assertArrayHasKey('name', $class->fieldMappings); + + + $this->assertArrayHasKey('type', $class->fieldMappings['id']); + $this->assertArrayHasKey('type', $class->fieldMappings['name']); + + $this->assertEquals('string', $class->fieldMappings['id']['type']); + $this->assertEquals('string', $class->fieldMappings['name']['type']); + + + + $this->assertArrayHasKey('fieldName', $class->fieldMappings['id']); + $this->assertArrayHasKey('fieldName', $class->fieldMappings['name']); + + $this->assertEquals('id', $class->fieldMappings['id']['fieldName']); + $this->assertEquals('name', $class->fieldMappings['name']['fieldName']); + + + + $this->assertArrayHasKey('columnName', $class->fieldMappings['id']); + $this->assertArrayHasKey('columnName', $class->fieldMappings['name']); + + $this->assertEquals('id', $class->fieldMappings['id']['columnName']); + $this->assertEquals('name', $class->fieldMappings['name']['columnName']); + + $this->assertEquals(ClassMetadataInfo::GENERATOR_TYPE_NONE, $class->generatorType); + } } /** diff --git a/tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php index e946a2628..9273f7c6b 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php @@ -199,6 +199,20 @@ class AnnotationDriverTest extends AbstractMappingDriverTest $cm = $factory->getMetadataFor('Doctrine\Tests\ORM\Mapping\ChildEntity'); } + + public function testInvalidFetchOptionThrowsException() + { + $annotationDriver = $this->_loadDriver(); + + $em = $this->_getTestEntityManager(); + $em->getConfiguration()->setMetadataDriverImpl($annotationDriver); + $factory = new \Doctrine\ORM\Mapping\ClassMetadataFactory(); + $factory->setEntityManager($em); + + $this->setExpectedException('Doctrine\ORM\Mapping\MappingException', + "Entity 'Doctrine\Tests\ORM\Mapping\InvalidFetchOption' has a mapping with invalid fetch mode 'eager"); + $cm = $factory->getMetadataFor('Doctrine\Tests\ORM\Mapping\InvalidFetchOption'); + } } /** @@ -310,4 +324,15 @@ class ChildEntity extends MiddleMappedSuperclass * @Column(type="string") */ private $text; -} \ No newline at end of file +} + +/** + * @Entity + */ +class InvalidFetchOption +{ + /** + * @OneToMany(targetEntity="Doctrine\Tests\Models\CMS\CmsUser", fetch="eager") + */ + private $collection; +} diff --git a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php index fed31d9c5..55726e387 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php @@ -54,7 +54,7 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $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); + $this->assertEquals(array('dql' => array('name'=>'dql','query'=>'foo','dql'=>'foo')), $cm->namedQueries); } public function testFieldIsNullable() diff --git a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.DDC1476.DDC1476EntityWithDefaultFieldType.php b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.DDC1476.DDC1476EntityWithDefaultFieldType.php new file mode 100644 index 000000000..56a99633a --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.DDC1476.DDC1476EntityWithDefaultFieldType.php @@ -0,0 +1,12 @@ +mapField(array( + 'id' => true, + 'fieldName' => 'id', +)); +$metadata->mapField(array( + 'fieldName' => 'name' +)); +$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_NONE); \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.DDC1476.DDC1476EntityWithDefaultFieldType.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.DDC1476.DDC1476EntityWithDefaultFieldType.dcm.xml new file mode 100644 index 000000000..29b5f1db5 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.DDC1476.DDC1476EntityWithDefaultFieldType.dcm.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.DDC1476.DDC1476EntityWithDefaultFieldType.dcm.yml b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.DDC1476.DDC1476EntityWithDefaultFieldType.dcm.yml new file mode 100644 index 000000000..3437a9b37 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.DDC1476.DDC1476EntityWithDefaultFieldType.dcm.yml @@ -0,0 +1,8 @@ +Doctrine\Tests\Models\DDC1476\DDC1476EntityWithDefaultFieldType: + type: entity + id: + id: + generator: + strategy: NONE + fields: + name: \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 8b501a50e..8eaaf37bb 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -17,11 +17,11 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase /** * Assert a valid SQL generation. - * + * * @param string $dqlToBeTested * @param string $sqlToBeConfirmed * @param array $queryHints - * @param array $queryParams + * @param array $queryParams */ public function assertSqlGeneration($dqlToBeTested, $sqlToBeConfirmed, array $queryHints = array(), array $queryParams = array()) { @@ -34,11 +34,11 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase $query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true) ->useQueryCache(false); - + foreach ($queryHints AS $name => $value) { $query->setHint($name, $value); } - + parent::assertEquals($sqlToBeConfirmed, $query->getSQL()); $query->free(); } catch (\Exception $e) { @@ -383,7 +383,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase "SELECT c0_.id AS id0, c0_.name AS name1, c0_.discr AS discr2 FROM company_persons c0_ WHERE c0_.discr IN ('employee')" ); } - + public function testSupportsInstanceOfExpressionInWherePartWithMultipleValues() { $this->assertSqlGeneration( @@ -391,7 +391,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase "SELECT c0_.id AS id0, c0_.name AS name1, c0_.discr AS discr2 FROM company_persons c0_ WHERE c0_.discr IN ('employee', 'manager')" ); } - + /** * @group DDC-1194 */ @@ -402,7 +402,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase "SELECT c0_.id AS id0, c0_.name AS name1, c0_.discr AS discr2 FROM company_persons c0_ WHERE c0_.discr IN ('employee')" ); } - + /** * @group DDC-1194 */ @@ -563,7 +563,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase $this->_em->getClassMetadata(get_class($person))->setIdentifierValues($person, array('id' => 101)); $q3->setParameter('param', $person); $this->assertEquals( - 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.title AS title2, c1_.car_id AS car_id3, c2_.salary AS salary4, c2_.department AS department5, c2_.startDate AS startDate6, c0_.discr AS discr7, c0_.spouse_id AS spouse_id8 FROM company_persons c0_ LEFT JOIN company_managers c1_ ON c0_.id = c1_.id LEFT JOIN company_employees c2_ ON c0_.id = c2_.id WHERE EXISTS (SELECT 1 FROM company_persons_friends c3_ INNER JOIN company_persons c4_ ON c3_.friend_id = c4_.id WHERE c3_.person_id = c0_.id AND c4_.id = ?)', + 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.title AS title2, c2_.salary AS salary3, c2_.department AS department4, c2_.startDate AS startDate5, c0_.discr AS discr6, c0_.spouse_id AS spouse_id7, c1_.car_id AS car_id8 FROM company_persons c0_ LEFT JOIN company_managers c1_ ON c0_.id = c1_.id LEFT JOIN company_employees c2_ ON c0_.id = c2_.id WHERE EXISTS (SELECT 1 FROM company_persons_friends c3_ INNER JOIN company_persons c4_ ON c3_.friend_id = c4_.id WHERE c3_.person_id = c0_.id AND c4_.id = ?)', $q3->getSql() ); } @@ -606,7 +606,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase . ')' ); } - + public function testExistsExpressionWithSimpleSelectReturningScalar() { $this->assertSqlGeneration( @@ -703,7 +703,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase "SELECT c0_.id AS id0, c0_.status AS status1, c0_.username AS username2, c0_.name AS name3, (SELECT COUNT(*) FROM cms_articles c1_ WHERE c1_.user_id = c0_.id) AS sclr4 FROM cms_users c0_ ORDER BY sclr4 ASC" ); } - + public function testOrderBySupportsSingleValuedPathExpressionOwningSide() { $this->assertSqlGeneration( @@ -711,7 +711,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase "SELECT c0_.id AS id0, c0_.topic AS topic1, c0_.text AS text2, c0_.version AS version3 FROM cms_articles c0_ ORDER BY c0_.user_id ASC" ); } - + /** * @expectedException Doctrine\ORM\Query\QueryException */ @@ -746,12 +746,12 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase $this->assertSqlGeneration( "SELECT b FROM Doctrine\Tests\Models\Generic\BooleanModel b WHERE b.booleanField = true", - "SELECT b0_.id AS id0, b0_.booleanField AS booleanField1 FROM boolean_model b0_ WHERE b0_.booleanField = true" + "SELECT b0_.id AS id0, b0_.booleanField AS booleanfield1 FROM boolean_model b0_ WHERE b0_.booleanField = true" ); $this->assertSqlGeneration( "SELECT b FROM Doctrine\Tests\Models\Generic\BooleanModel b WHERE b.booleanField = false", - "SELECT b0_.id AS id0, b0_.booleanField AS booleanField1 FROM boolean_model b0_ WHERE b0_.booleanField = false" + "SELECT b0_.id AS id0, b0_.booleanField AS booleanfield1 FROM boolean_model b0_ WHERE b0_.booleanField = false" ); $this->_em->getConnection()->setDatabasePlatform($oldPlat); @@ -894,7 +894,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase $this->assertSqlGeneration( "SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.username = 'gblanco'", - "SELECT c0_.id AS id0, c0_.status AS status1, c0_.username AS username2, c0_.name AS name3 ". + "SELECT c0_.id AS ID0, c0_.status AS STATUS1, c0_.username AS USERNAME2, c0_.name AS NAME3 ". "FROM cms_users c0_ WHERE c0_.username = 'gblanco' FOR UPDATE", array(Query::HINT_LOCK_MODE => \Doctrine\DBAL\LockMode::PESSIMISTIC_READ) ); @@ -948,7 +948,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase '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' ); } - + public function testCaseContainingNullIf() { $this->assertSqlGeneration( @@ -956,7 +956,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase 'SELECT NULLIF(c0_.id, c0_.name) AS sclr0 FROM cms_groups c0_' ); } - + public function testCaseContainingCoalesce() { $this->assertSqlGeneration( @@ -1026,248 +1026,281 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase "SELECT d0_.article_id AS article_id0, d0_.title AS title1, d1_.article_id AS article_id2, d1_.title AS title3 FROM DDC117Link d2_ INNER JOIN DDC117Article d0_ ON d2_.target_id = d0_.article_id INNER JOIN DDC117Article d1_ ON d2_.source_id = d1_.article_id" ); } - + public function testGeneralCaseWithSingleWhenClause() { $this->assertSqlGeneration( - "SELECT g.id, CASE WHEN ((g.id / 2) > 18) THEN 1 ELSE 0 END AS test FROM Doctrine\Tests\Models\CMS\CmsGroup g", + "SELECT g.id, CASE WHEN ((g.id / 2) > 18) THEN 1 ELSE 0 END AS test FROM Doctrine\Tests\Models\CMS\CmsGroup g", "SELECT c0_.id AS id0, CASE WHEN (c0_.id / 2 > 18) THEN 1 ELSE 0 END AS sclr1 FROM cms_groups c0_" ); } - + public function testGeneralCaseWithMultipleWhenClause() { $this->assertSqlGeneration( - "SELECT g.id, CASE WHEN (g.id / 2 < 10) THEN 2 WHEN ((g.id / 2) > 20) THEN 1 ELSE 0 END AS test FROM Doctrine\Tests\Models\CMS\CmsGroup g", + "SELECT g.id, CASE WHEN (g.id / 2 < 10) THEN 2 WHEN ((g.id / 2) > 20) THEN 1 ELSE 0 END AS test FROM Doctrine\Tests\Models\CMS\CmsGroup g", "SELECT c0_.id AS id0, CASE WHEN (c0_.id / 2 < 10) THEN 2 WHEN (c0_.id / 2 > 20) THEN 1 ELSE 0 END AS sclr1 FROM cms_groups c0_" ); } - + public function testSimpleCaseWithSingleWhenClause() { $this->assertSqlGeneration( - "SELECT g FROM Doctrine\Tests\Models\CMS\CmsGroup g WHERE g.id = CASE g.name WHEN 'admin' THEN 1 ELSE 2 END", + "SELECT g FROM Doctrine\Tests\Models\CMS\CmsGroup g WHERE g.id = CASE g.name WHEN 'admin' THEN 1 ELSE 2 END", "SELECT c0_.id AS id0, c0_.name AS name1 FROM cms_groups c0_ WHERE c0_.id = CASE c0_.name WHEN admin THEN 1 ELSE 2 END" ); } - + public function testSimpleCaseWithMultipleWhenClause() { $this->assertSqlGeneration( - "SELECT g FROM Doctrine\Tests\Models\CMS\CmsGroup g WHERE g.id = (CASE g.name WHEN 'admin' THEN 1 WHEN 'moderator' THEN 2 ELSE 3 END)", + "SELECT g FROM Doctrine\Tests\Models\CMS\CmsGroup g WHERE g.id = (CASE g.name WHEN 'admin' THEN 1 WHEN 'moderator' THEN 2 ELSE 3 END)", "SELECT c0_.id AS id0, c0_.name AS name1 FROM cms_groups c0_ WHERE c0_.id = CASE c0_.name WHEN admin THEN 1 WHEN moderator THEN 2 ELSE 3 END" ); } - + public function testGeneralCaseWithSingleWhenClauseInSubselect() { $this->assertSqlGeneration( - "SELECT g FROM Doctrine\Tests\Models\CMS\CmsGroup g WHERE g.id IN (SELECT CASE WHEN ((g2.id / 2) > 18) THEN 2 ELSE 1 END FROM Doctrine\Tests\Models\CMS\CmsGroup g2)", + "SELECT g FROM Doctrine\Tests\Models\CMS\CmsGroup g WHERE g.id IN (SELECT CASE WHEN ((g2.id / 2) > 18) THEN 2 ELSE 1 END FROM Doctrine\Tests\Models\CMS\CmsGroup g2)", "SELECT c0_.id AS id0, c0_.name AS name1 FROM cms_groups c0_ WHERE c0_.id IN (SELECT CASE WHEN (c1_.id / 2 > 18) THEN 2 ELSE 1 END AS sclr2 FROM cms_groups c1_)" ); } - + public function testGeneralCaseWithMultipleWhenClauseInSubselect() { $this->assertSqlGeneration( - "SELECT g FROM Doctrine\Tests\Models\CMS\CmsGroup g WHERE g.id IN (SELECT CASE WHEN (g.id / 2 < 10) THEN 3 WHEN ((g.id / 2) > 20) THEN 2 ELSE 1 END FROM Doctrine\Tests\Models\CMS\CmsGroup g2)", + "SELECT g FROM Doctrine\Tests\Models\CMS\CmsGroup g WHERE g.id IN (SELECT CASE WHEN (g.id / 2 < 10) THEN 3 WHEN ((g.id / 2) > 20) THEN 2 ELSE 1 END FROM Doctrine\Tests\Models\CMS\CmsGroup g2)", "SELECT c0_.id AS id0, c0_.name AS name1 FROM cms_groups c0_ WHERE c0_.id IN (SELECT CASE WHEN (c0_.id / 2 < 10) THEN 3 WHEN (c0_.id / 2 > 20) THEN 2 ELSE 1 END AS sclr2 FROM cms_groups c1_)" ); } - + public function testSimpleCaseWithSingleWhenClauseInSubselect() { $this->assertSqlGeneration( - "SELECT g FROM Doctrine\Tests\Models\CMS\CmsGroup g WHERE g.id IN (SELECT CASE g2.name WHEN 'admin' THEN 1 ELSE 2 END FROM Doctrine\Tests\Models\CMS\CmsGroup g2)", + "SELECT g FROM Doctrine\Tests\Models\CMS\CmsGroup g WHERE g.id IN (SELECT CASE g2.name WHEN 'admin' THEN 1 ELSE 2 END FROM Doctrine\Tests\Models\CMS\CmsGroup g2)", "SELECT c0_.id AS id0, c0_.name AS name1 FROM cms_groups c0_ WHERE c0_.id IN (SELECT CASE c1_.name WHEN admin THEN 1 ELSE 2 END AS sclr2 FROM cms_groups c1_)" ); } - + public function testSimpleCaseWithMultipleWhenClauseInSubselect() { $this->assertSqlGeneration( - "SELECT g FROM Doctrine\Tests\Models\CMS\CmsGroup g WHERE g.id IN (SELECT CASE g2.name WHEN 'admin' THEN 1 WHEN 'moderator' THEN 2 ELSE 3 END FROM Doctrine\Tests\Models\CMS\CmsGroup g2)", + "SELECT g FROM Doctrine\Tests\Models\CMS\CmsGroup g WHERE g.id IN (SELECT CASE g2.name WHEN 'admin' THEN 1 WHEN 'moderator' THEN 2 ELSE 3 END FROM Doctrine\Tests\Models\CMS\CmsGroup g2)", "SELECT c0_.id AS id0, c0_.name AS name1 FROM cms_groups c0_ WHERE c0_.id IN (SELECT CASE c1_.name WHEN admin THEN 1 WHEN moderator THEN 2 ELSE 3 END AS sclr2 FROM cms_groups c1_)" ); } - + /** * @group DDC-1339 */ public function testIdentityFunctionInSelectClause() { $this->assertSqlGeneration( - "SELECT IDENTITY(u.email) as email_id FROM Doctrine\Tests\Models\CMS\CmsUser u", + "SELECT IDENTITY(u.email) as email_id FROM Doctrine\Tests\Models\CMS\CmsUser u", "SELECT c0_.email_id AS sclr0 FROM cms_users c0_" ); } - + /** * @group DDC-1339 */ public function testIdentityFunctionDoesNotAcceptStateField() { $this->assertInvalidSqlGeneration( - "SELECT IDENTITY(u.name) as name FROM Doctrine\Tests\Models\CMS\CmsUser u", + "SELECT IDENTITY(u.name) as name FROM Doctrine\Tests\Models\CMS\CmsUser u", "Doctrine\ORM\Query\QueryException" ); } - + /** * @group DDC-1389 */ public function testInheritanceTypeJoinInRootClassWithDisabledForcePartialLoad() { $this->assertSqlGeneration( - 'SELECT p FROM Doctrine\Tests\Models\Company\CompanyPerson p', - 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.title AS title2, c1_.car_id AS car_id3, c2_.salary AS salary4, c2_.department AS department5, c2_.startDate AS startDate6, c0_.discr AS discr7, c0_.spouse_id AS spouse_id8 FROM company_persons c0_ LEFT JOIN company_managers c1_ ON c0_.id = c1_.id LEFT JOIN company_employees c2_ ON c0_.id = c2_.id', + 'SELECT p FROM Doctrine\Tests\Models\Company\CompanyPerson p', + 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.title AS title2, c2_.salary AS salary3, c2_.department AS department4, c2_.startDate AS startDate5, c0_.discr AS discr6, c0_.spouse_id AS spouse_id7, c1_.car_id AS car_id8 FROM company_persons c0_ LEFT JOIN company_managers c1_ ON c0_.id = c1_.id LEFT JOIN company_employees c2_ ON c0_.id = c2_.id', array(Query::HINT_FORCE_PARTIAL_LOAD => false) ); } - + /** * @group DDC-1389 */ public function testInheritanceTypeJoinInRootClassWithEnabledForcePartialLoad() { $this->assertSqlGeneration( - 'SELECT p FROM Doctrine\Tests\Models\Company\CompanyPerson p', + 'SELECT p FROM Doctrine\Tests\Models\Company\CompanyPerson p', 'SELECT c0_.id AS id0, c0_.name AS name1, c0_.discr AS discr2 FROM company_persons c0_', array(Query::HINT_FORCE_PARTIAL_LOAD => true) ); } - + /** * @group DDC-1389 */ public function testInheritanceTypeJoinInChildClassWithDisabledForcePartialLoad() { $this->assertSqlGeneration( - 'SELECT e FROM Doctrine\Tests\Models\Company\CompanyEmployee e', - 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.salary AS salary2, c1_.department AS department3, c1_.startDate AS startDate4, c2_.title AS title5, c2_.car_id AS car_id6, c0_.discr AS discr7, c0_.spouse_id AS spouse_id8 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id LEFT JOIN company_managers c2_ ON c1_.id = c2_.id', + 'SELECT e FROM Doctrine\Tests\Models\Company\CompanyEmployee e', + 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.salary AS salary2, c1_.department AS department3, c1_.startDate AS startDate4, c2_.title AS title5, c0_.discr AS discr6, c0_.spouse_id AS spouse_id7, c2_.car_id AS car_id8 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id LEFT JOIN company_managers c2_ ON c1_.id = c2_.id', array(Query::HINT_FORCE_PARTIAL_LOAD => false) ); } - + /** * @group DDC-1389 */ public function testInheritanceTypeJoinInChildClassWithEnabledForcePartialLoad() { $this->assertSqlGeneration( - 'SELECT e FROM Doctrine\Tests\Models\Company\CompanyEmployee e', + 'SELECT e FROM Doctrine\Tests\Models\Company\CompanyEmployee e', 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.salary AS salary2, c1_.department AS department3, c1_.startDate AS startDate4, c0_.discr AS discr5 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id', array(Query::HINT_FORCE_PARTIAL_LOAD => true) ); } - + /** * @group DDC-1389 */ public function testInheritanceTypeJoinInLeafClassWithDisabledForcePartialLoad() { $this->assertSqlGeneration( - 'SELECT m FROM Doctrine\Tests\Models\Company\CompanyManager m', + 'SELECT m FROM Doctrine\Tests\Models\Company\CompanyManager m', 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.salary AS salary2, c1_.department AS department3, c1_.startDate AS startDate4, c2_.title AS title5, c0_.discr AS discr6, c0_.spouse_id AS spouse_id7, c2_.car_id AS car_id8 FROM company_managers c2_ INNER JOIN company_employees c1_ ON c2_.id = c1_.id INNER JOIN company_persons c0_ ON c2_.id = c0_.id', array(Query::HINT_FORCE_PARTIAL_LOAD => false) ); } - + /** * @group DDC-1389 */ public function testInheritanceTypeJoinInLeafClassWithEnabledForcePartialLoad() { $this->assertSqlGeneration( - 'SELECT m FROM Doctrine\Tests\Models\Company\CompanyManager m', + 'SELECT m FROM Doctrine\Tests\Models\Company\CompanyManager m', 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.salary AS salary2, c1_.department AS department3, c1_.startDate AS startDate4, c2_.title AS title5, c0_.discr AS discr6 FROM company_managers c2_ INNER JOIN company_employees c1_ ON c2_.id = c1_.id INNER JOIN company_persons c0_ ON c2_.id = c0_.id', array(Query::HINT_FORCE_PARTIAL_LOAD => true) ); } - + /** * @group DDC-1389 */ public function testInheritanceTypeSingleTableInRootClassWithDisabledForcePartialLoad() { $this->assertSqlGeneration( - 'SELECT c FROM Doctrine\Tests\Models\Company\CompanyContract c', + 'SELECT c FROM Doctrine\Tests\Models\Company\CompanyContract c', "SELECT c0_.id AS id0, c0_.completed AS completed1, c0_.fixPrice AS fixPrice2, c0_.hoursWorked AS hoursWorked3, c0_.pricePerHour AS pricePerHour4, c0_.maxPrice AS maxPrice5, c0_.discr AS discr6, c0_.salesPerson_id AS salesPerson_id7 FROM company_contracts c0_ WHERE c0_.discr IN ('fix', 'flexible', 'flexultra')", array(Query::HINT_FORCE_PARTIAL_LOAD => false) ); } - + /** * @group DDC-1389 */ public function testInheritanceTypeSingleTableInRootClassWithEnabledForcePartialLoad() { $this->assertSqlGeneration( - 'SELECT c FROM Doctrine\Tests\Models\Company\CompanyContract c', + 'SELECT c FROM Doctrine\Tests\Models\Company\CompanyContract c', "SELECT c0_.id AS id0, c0_.completed AS completed1, c0_.fixPrice AS fixPrice2, c0_.hoursWorked AS hoursWorked3, c0_.pricePerHour AS pricePerHour4, c0_.maxPrice AS maxPrice5, c0_.discr AS discr6 FROM company_contracts c0_ WHERE c0_.discr IN ('fix', 'flexible', 'flexultra')", array(Query::HINT_FORCE_PARTIAL_LOAD => true) ); } - + /** * @group DDC-1389 */ public function testInheritanceTypeSingleTableInChildClassWithDisabledForcePartialLoad() { $this->assertSqlGeneration( - 'SELECT fc FROM Doctrine\Tests\Models\Company\CompanyFlexContract fc', + 'SELECT fc FROM Doctrine\Tests\Models\Company\CompanyFlexContract fc', "SELECT c0_.id AS id0, c0_.completed AS completed1, c0_.hoursWorked AS hoursWorked2, c0_.pricePerHour AS pricePerHour3, c0_.maxPrice AS maxPrice4, c0_.discr AS discr5, c0_.salesPerson_id AS salesPerson_id6 FROM company_contracts c0_ WHERE c0_.discr IN ('flexible', 'flexultra')", array(Query::HINT_FORCE_PARTIAL_LOAD => false) ); } - + /** * @group DDC-1389 */ public function testInheritanceTypeSingleTableInChildClassWithEnabledForcePartialLoad() { $this->assertSqlGeneration( - 'SELECT fc FROM Doctrine\Tests\Models\Company\CompanyFlexContract fc', + 'SELECT fc FROM Doctrine\Tests\Models\Company\CompanyFlexContract fc', "SELECT c0_.id AS id0, c0_.completed AS completed1, c0_.hoursWorked AS hoursWorked2, c0_.pricePerHour AS pricePerHour3, c0_.maxPrice AS maxPrice4, c0_.discr AS discr5 FROM company_contracts c0_ WHERE c0_.discr IN ('flexible', 'flexultra')", array(Query::HINT_FORCE_PARTIAL_LOAD => true) ); } - + /** * @group DDC-1389 */ public function testInheritanceTypeSingleTableInLeafClassWithDisabledForcePartialLoad() { $this->assertSqlGeneration( - 'SELECT fuc FROM Doctrine\Tests\Models\Company\CompanyFlexUltraContract fuc', + 'SELECT fuc FROM Doctrine\Tests\Models\Company\CompanyFlexUltraContract fuc', "SELECT c0_.id AS id0, c0_.completed AS completed1, c0_.hoursWorked AS hoursWorked2, c0_.pricePerHour AS pricePerHour3, c0_.maxPrice AS maxPrice4, c0_.discr AS discr5, c0_.salesPerson_id AS salesPerson_id6 FROM company_contracts c0_ WHERE c0_.discr IN ('flexultra')", array(Query::HINT_FORCE_PARTIAL_LOAD => false) ); } - + /** * @group DDC-1389 */ public function testInheritanceTypeSingleTableInLeafClassWithEnabledForcePartialLoad() { $this->assertSqlGeneration( - 'SELECT fuc FROM Doctrine\Tests\Models\Company\CompanyFlexUltraContract fuc', + 'SELECT fuc FROM Doctrine\Tests\Models\Company\CompanyFlexUltraContract fuc', "SELECT c0_.id AS id0, c0_.completed AS completed1, c0_.hoursWorked AS hoursWorked2, c0_.pricePerHour AS pricePerHour3, c0_.maxPrice AS maxPrice4, c0_.discr AS discr5 FROM company_contracts c0_ WHERE c0_.discr IN ('flexultra')", array(Query::HINT_FORCE_PARTIAL_LOAD => true) ); } - + /** * @group DDC-1161 */ public function testSelfReferenceWithOneToOneDoesNotDuplicateAlias() { $this->assertSqlGeneration( - 'SELECT p, pp FROM Doctrine\Tests\Models\Company\CompanyPerson p JOIN p.spouse pp', - "SELECT c0_.id AS id0, c0_.name AS name1, c1_.title AS title2, c1_.car_id AS car_id3, c2_.salary AS salary4, c2_.department AS department5, c2_.startDate AS startDate6, c3_.id AS id7, c3_.name AS name8, c4_.title AS title9, c4_.car_id AS car_id10, c5_.salary AS salary11, c5_.department AS department12, c5_.startDate AS startDate13, c0_.discr AS discr14, c0_.spouse_id AS spouse_id15, c3_.discr AS discr16, c3_.spouse_id AS spouse_id17 FROM company_persons c0_ LEFT JOIN company_managers c1_ ON c0_.id = c1_.id LEFT JOIN company_employees c2_ ON c0_.id = c2_.id INNER JOIN company_persons c3_ ON c0_.spouse_id = c3_.id LEFT JOIN company_managers c4_ ON c3_.id = c4_.id LEFT JOIN company_employees c5_ ON c3_.id = c5_.id", + 'SELECT p, pp FROM Doctrine\Tests\Models\Company\CompanyPerson p JOIN p.spouse pp', + "SELECT c0_.id AS id0, c0_.name AS name1, c1_.title AS title2, c2_.salary AS salary3, c2_.department AS department4, c2_.startDate AS startDate5, c3_.id AS id6, c3_.name AS name7, c4_.title AS title8, c5_.salary AS salary9, c5_.department AS department10, c5_.startDate AS startDate11, c0_.discr AS discr12, c0_.spouse_id AS spouse_id13, c1_.car_id AS car_id14, c3_.discr AS discr15, c3_.spouse_id AS spouse_id16, c4_.car_id AS car_id17 FROM company_persons c0_ LEFT JOIN company_managers c1_ ON c0_.id = c1_.id LEFT JOIN company_employees c2_ ON c0_.id = c2_.id INNER JOIN company_persons c3_ ON c0_.spouse_id = c3_.id LEFT JOIN company_managers c4_ ON c3_.id = c4_.id LEFT JOIN company_employees c5_ ON c3_.id = c5_.id", array(Query::HINT_FORCE_PARTIAL_LOAD => false) ); } + + /** + * @group DDC-1384 + */ + public function testAliasDoesNotExceedPlatformDefinedLength() + { + $this->assertSqlGeneration( + 'SELECT m FROM ' . __NAMESPACE__ . '\\DDC1384Model m', + "SELECT d0_.aVeryLongIdentifierThatShouldBeShortenedByTheSQLWalker_fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo AS fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0 FROM DDC1384Model d0_" + ); + } + + /** + * @group DDC-331 + * @group DDC-1384 + */ + public function testIssue331() + { + $this->assertSqlGeneration( + 'SELECT e.name FROM Doctrine\Tests\Models\Company\CompanyEmployee e', + 'SELECT c0_.name AS name0 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id' + ); + } + /** + * @group DDC-1435 + */ + public function testForeignKeyAsPrimaryKeySubselect() + { + $this->assertSqlGeneration( + "SELECT s FROM Doctrine\Tests\Models\DDC117\DDC117Article s WHERE EXISTS (SELECT r FROM Doctrine\Tests\Models\DDC117\DDC117Reference r WHERE r.source = s)", + "SELECT d0_.article_id AS article_id0, d0_.title AS title1 FROM DDC117Article d0_ WHERE EXISTS (SELECT d1_.source_id, d1_.target_id FROM DDC117Reference d1_ WHERE d1_.source_id = d0_.article_id)" + ); + } } @@ -1294,7 +1327,19 @@ class MyAbsFunction extends \Doctrine\ORM\Query\AST\Functions\FunctionNode $parser->match(\Doctrine\ORM\Query\Lexer::T_OPEN_PARENTHESIS); $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); - + $parser->match(\Doctrine\ORM\Query\Lexer::T_CLOSE_PARENTHESIS); } } +/** + * @Entity + */ +class DDC1384Model +{ + /** + * @Id + * @Column(type="integer") + * @GeneratedValue + */ + protected $aVeryLongIdentifierThatShouldBeShortenedByTheSQLWalker_fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo; +} diff --git a/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php b/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php index 64bb03f36..0fc3bb636 100644 --- a/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php @@ -71,4 +71,88 @@ class SchemaValidatorTest extends \Doctrine\Tests\OrmTestCase )); $this->validator->validateMapping(); } + + /** + * @group DDC-1439 + */ + public function testInvalidManyToManyJoinColumnSchema() + { + $class1 = $this->em->getClassMetadata(__NAMESPACE__ . '\InvalidEntity1'); + $class2 = $this->em->getClassMetadata(__NAMESPACE__ . '\InvalidEntity2'); + + $ce = $this->validator->validateClass($class1); + + $this->assertEquals( + array( + "The inverse join columns of the many-to-many table 'Entity1Entity2' have to contain to ALL identifier columns of the target entity 'Doctrine\Tests\ORM\Tools\InvalidEntity2', however 'key4' are missing.", + "The join columns of the many-to-many table 'Entity1Entity2' have to contain to ALL identifier columns of the source entity 'Doctrine\Tests\ORM\Tools\InvalidEntity1', however 'key2' are missing." + ), + $ce + ); + } + + /** + * @group DDC-1439 + */ + public function testInvalidToOneJoinColumnSchema() + { + $class1 = $this->em->getClassMetadata(__NAMESPACE__ . '\InvalidEntity1'); + $class2 = $this->em->getClassMetadata(__NAMESPACE__ . '\InvalidEntity2'); + + $ce = $this->validator->validateClass($class2); + + $this->assertEquals( + array( + "The referenced column name 'id' does not have a corresponding field with this column name on the class 'Doctrine\Tests\ORM\Tools\InvalidEntity1'.", + "The join columns of the association 'assoc' have to match to ALL identifier columns of the source entity 'Doctrine\Tests\ORM\Tools\InvalidEntity2', however 'key3, key4' are missing." + ), + $ce + ); + } +} + +/** + * @Entity + */ +class InvalidEntity1 +{ + /** + * @Id @Column + */ + protected $key1; + /** + * @Id @Column + */ + protected $key2; + /** + * @ManyToMany (targetEntity="InvalidEntity2") + * @JoinTable (name="Entity1Entity2", + * joinColumns={@JoinColumn(name="key1", referencedColumnName="key1")}, + * inverseJoinColumns={@JoinColumn(name="key3", referencedColumnName="key3")} + * ) + */ + protected $entity2; +} + +/** + * @Entity + */ +class InvalidEntity2 +{ + /** + * @Id @Column + * @GeneratedValue(strategy="AUTO") + */ + protected $key3; + + /** + * @Id @Column + * @GeneratedValue(strategy="AUTO") + */ + protected $key4; + + /** + * @ManyToOne(targetEntity="InvalidEntity1") + */ + protected $assoc; } \ No newline at end of file diff --git a/tests/travis/mysql.travis.xml b/tests/travis/mysql.travis.xml new file mode 100644 index 000000000..f17a4b87d --- /dev/null +++ b/tests/travis/mysql.travis.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + ./../Doctrine/Tests/ORM + + + + + performance + locking_functional + + + + + diff --git a/tests/travis/pgsql.travis.xml b/tests/travis/pgsql.travis.xml new file mode 100644 index 000000000..fa0581acb --- /dev/null +++ b/tests/travis/pgsql.travis.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + ./../Doctrine/Tests/ORM + + + + + + performance + locking_functional + + + + \ No newline at end of file diff --git a/tests/travis/sqlite.travis.xml b/tests/travis/sqlite.travis.xml new file mode 100644 index 000000000..5d310c327 --- /dev/null +++ b/tests/travis/sqlite.travis.xml @@ -0,0 +1,15 @@ + + + + + ./../Doctrine/Tests/ORM + + + + + performance + locking_functional + + + + \ No newline at end of file