From cb0e5dbff32711093e4f7055ab3d9daaf036d401 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 16 Oct 2011 12:46:17 +0200 Subject: [PATCH 01/77] DDC-1385 - Fixed Mixed result hydration using INDEX BY to work on the top-level indexes, not some weird result. This is a BC break to those that actually use this crazy logic, sorry for that :-) --- .../ORM/Internal/Hydration/ObjectHydrator.php | 22 +++--- .../ORM/Hydration/ObjectHydratorTest.php | 69 ++++++++++++++++--- 2 files changed, 70 insertions(+), 21 deletions(-) diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 1287a138b..b07f1ae48 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -430,19 +430,16 @@ class ObjectHydrator extends AbstractHydrator $element = $this->_getEntity($rowData[$dqlAlias], $dqlAlias); if (isset($this->_rsm->indexByMap[$dqlAlias])) { $field = $this->_rsm->indexByMap[$dqlAlias]; - $key = $this->_ce[$entityName]->reflFields[$field]->getValue($element); + $resultKey = $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; + $element = array(0 => $element); } + $result[$resultKey] = $element; + $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey; + if (isset($this->_hints['collection'])) { - $this->_hints['collection']->hydrateSet($key, $element); + $this->_hints['collection']->hydrateSet($resultKey, $element); } } else { if ($this->_rsm->isMixed) { @@ -464,6 +461,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; @@ -474,8 +472,12 @@ class ObjectHydrator extends AbstractHydrator // Append scalar values to mixed result sets if (isset($scalars)) { + if ( ! isset($resultKey) ) { + $resultKey = $this->_resultCounter - 1; + } + foreach ($scalars as $name => $value) { - $result[$this->_resultCounter - 1][$name] = $value; + $result[$resultKey][$name] = $value; } } } diff --git a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php index 581a3504a..68a76cc80 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php @@ -352,24 +352,24 @@ class ObjectHydratorTest extends HydrationTestCase $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']); - $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); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[2][0]); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][0]->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][0]->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][0]->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][0]->phonenumbers['42'])); + $this->assertTrue(isset($result[1][0]->phonenumbers['43'])); + $this->assertTrue(isset($result[2][0]->phonenumbers['91'])); } /** @@ -1169,4 +1169,51 @@ class ObjectHydratorTest extends HydrationTestCase $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsAddress', $result[0][0]->address); $this->assertNull($result[1][0]->address); } + + /** + * @group DDC-1385 + */ + public function testIndexByAndMixedResult() + { + $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $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][0]->id); + $this->assertTrue(isset($result[2])); + $this->assertEquals(2, $result[2][0]->id); + } + + /** + * @group DDC-1385 + */ + public function testIndexByAndScalarResult() + { + $rsm = new ResultSetMapping; + } } From 2730f64d90cb0ec97fe35cea950d530722bbafbc Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 16 Oct 2011 16:13:59 +0200 Subject: [PATCH 02/77] DDC-1385 - Refactor ObjectHydrator --- .../ORM/Internal/Hydration/ObjectHydrator.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index b07f1ae48..69b441480 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -428,32 +428,32 @@ 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(0 => $element); + } + if (isset($this->_rsm->indexByMap[$dqlAlias])) { $field = $this->_rsm->indexByMap[$dqlAlias]; - $resultKey = $this->_ce[$entityName]->reflFields[$field]->getValue($element); - if ($this->_rsm->isMixed) { - $element = array(0 => $element); - } - - $result[$resultKey] = $element; - $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey; + $resultKey = $rowData[$dqlAlias][$field]; if (isset($this->_hints['collection'])) { $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; From ee924ffaba4dc8b666dc2b6070ce1b4b5f7f1308 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 16 Oct 2011 16:27:50 +0200 Subject: [PATCH 03/77] DDC-1385 - Fix scalar handling for array hydrator --- .../ORM/Internal/Hydration/ArrayHydrator.php | 32 ++++++----- .../Tests/ORM/Hydration/ArrayHydratorTest.php | 55 ++++++++++++++++--- .../ORM/Hydration/ObjectHydratorTest.php | 8 --- 3 files changed, 64 insertions(+), 31 deletions(-) diff --git a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php index 4b1c21c6f..cd30a37c0 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php @@ -174,26 +174,24 @@ class ArrayHydrator extends AbstractHydrator // Check for an existing element if ($this->_isSimpleQuery || ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { $element = $rowData[$dqlAlias]; + if ($this->_rsm->isMixed) { + $element = array(0 => $element); + } + 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; - } + $resultKey = $rowData[$dqlAlias][$field]; + $result[$resultKey] = $element; } else { - if ($this->_rsm->isMixed) { - $result[] = array($element); - ++$this->_resultCounter; - } else { - $result[] = $element; - } + $resultKey = $this->_resultCounter; + $result[] = $element; + ++$this->_resultCounter; } - end($result); - $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = key($result); + + $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 +203,12 @@ class ArrayHydrator extends AbstractHydrator // Append scalar values to mixed result sets if (isset($scalars)) { + if ( ! isset($resultKey) ) { + $resultKey = $this->_resultCounter - 1; + } + foreach ($scalars as $name => $value) { - $result[$this->_resultCounter - 1][$name] = $value; + $result[$resultKey][$name] = $value; } } } diff --git a/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php index a318f78af..616f19146 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php @@ -256,20 +256,20 @@ class ArrayHydratorTest extends HydrationTestCase $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][0]['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][0]['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][0]['phonenumbers']['42'])); + $this->assertTrue(isset($result[1][0]['phonenumbers']['43'])); + $this->assertTrue(isset($result[2][0]['phonenumbers']['91'])); } /** @@ -817,4 +817,43 @@ class ArrayHydratorTest extends HydrationTestCase $this->assertEquals(array('id' => 2, 'status' => 'developer'), $result[2][0]); $this->assertNull($result[3][0]); } + + /** + * @group DDC-1385 + */ + public function testIndexByAndMixedResult() + { + $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $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][0]['id']); + $this->assertTrue(isset($result[2])); + $this->assertEquals(2, $result[2][0]['id']); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php index 68a76cc80..d2d6f24ce 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php @@ -1208,12 +1208,4 @@ class ObjectHydratorTest extends HydrationTestCase $this->assertTrue(isset($result[2])); $this->assertEquals(2, $result[2][0]->id); } - - /** - * @group DDC-1385 - */ - public function testIndexByAndScalarResult() - { - $rsm = new ResultSetMapping; - } } From 8466060797f0f9c85dd8f9d6b36906c4cbf2e8f7 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 16 Oct 2011 19:23:20 +0200 Subject: [PATCH 04/77] DDC-1385 - Add INDEX BY scalar variables on the top-level --- .../ORM/Internal/Hydration/ArrayHydrator.php | 18 ++++++---- .../ORM/Internal/Hydration/ObjectHydrator.php | 17 +++++---- lib/Doctrine/ORM/Query/ResultSetMapping.php | 36 ++++++++++++++++++- lib/Doctrine/ORM/Query/SqlWalker.php | 25 ++++++++++--- .../ORM/Hydration/ObjectHydratorTest.php | 28 +++++++++++++++ 5 files changed, 104 insertions(+), 20 deletions(-) diff --git a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php index cd30a37c0..cbc77d2d2 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php @@ -66,12 +66,12 @@ class ArrayHydrator extends AbstractHydrator } /** @override */ - protected function _hydrateRow(array $data, array &$cache, array &$result) + protected function _hydrateRow(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'])) { @@ -128,8 +128,7 @@ class ArrayHydrator extends AbstractHydrator 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; } @@ -167,6 +166,7 @@ class ArrayHydrator extends AbstractHydrator } else { $result[] = null; } + $resultKey = $this->_resultCounter; ++$this->_resultCounter; continue; } @@ -179,8 +179,7 @@ class ArrayHydrator extends AbstractHydrator } if (isset($this->_rsm->indexByMap[$dqlAlias])) { - $field = $this->_rsm->indexByMap[$dqlAlias]; - $resultKey = $rowData[$dqlAlias][$field]; + $resultKey = $row[$this->_rsm->indexByMap[$dqlAlias]]; $result[$resultKey] = $element; } else { $resultKey = $this->_resultCounter; @@ -204,7 +203,12 @@ class ArrayHydrator extends AbstractHydrator // Append scalar values to mixed result sets if (isset($scalars)) { if ( ! isset($resultKey) ) { - $resultKey = $this->_resultCounter - 1; + // 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) { diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 69b441480..4cbba6bb7 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -273,13 +273,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 _hydrateRow(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 +352,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 { @@ -421,6 +420,7 @@ class ObjectHydrator extends AbstractHydrator } else { $result[] = null; } + $resultKey = $this->_resultCounter; ++$this->_resultCounter; continue; } @@ -433,8 +433,7 @@ class ObjectHydrator extends AbstractHydrator } if (isset($this->_rsm->indexByMap[$dqlAlias])) { - $field = $this->_rsm->indexByMap[$dqlAlias]; - $resultKey = $rowData[$dqlAlias][$field]; + $resultKey = $row[$this->_rsm->indexByMap[$dqlAlias]]; if (isset($this->_hints['collection'])) { $this->_hints['collection']->hydrateSet($resultKey, $element); @@ -473,7 +472,11 @@ class ObjectHydrator extends AbstractHydrator // Append scalar values to mixed result sets if (isset($scalars)) { if ( ! isset($resultKey) ) { - $resultKey = $this->_resultCounter - 1; + if (isset($this->_rsm->indexByMap['scalars'])) { + $resultKey = $row[$this->_rsm->indexByMap['scalars']]; + } else { + $resultKey = $this->_resultCounter - 1; + } } foreach ($scalars as $name => $value) { diff --git a/lib/Doctrine/ORM/Query/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index 3a086e2cd..fcfc9b834 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMapping.php +++ b/lib/Doctrine/ORM/Query/ResultSetMapping.php @@ -157,7 +157,41 @@ 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) { + $this->addIndexByColumn($alias, $columnName); + $found = true; + break; + } + } + if (!$found) { + throw new \LogicException("Cannot add index by for dql alias " . $alias . " and field " . + $fieldName . " without calling addFieldResult() for them before."); + } + } + + /** + * 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; } /** diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 188350197..e98e0db31 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -71,6 +71,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; @@ -381,7 +388,7 @@ class SqlWalker implements TreeWalker } else if ($lockMode == LockMode::OPTIMISTIC) { foreach ($this->_selectedClasses AS $class) { if ( ! $class->isVersioned) { - throw \Doctrine\ORM\OptimisticLockException::lockFailed(); + throw \Doctrine\ORM\OptimisticLockException::lockFailed($class->name); } } } @@ -619,10 +626,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)); @@ -1010,6 +1024,7 @@ class SqlWalker implements TreeWalker if ( ! $hidden) { $this->_rsm->addScalarResult($columnAlias, $resultAlias); + $this->_scalarFields[$dqlAlias][$fieldName] = $columnAlias; } } else if ($expr instanceof AST\AggregateExpression) { if ( ! $selectExpression->fieldIdentificationVariable) { diff --git a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php index d2d6f24ce..65cd57c95 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php @@ -1208,4 +1208,32 @@ class ObjectHydratorTest extends HydrationTestCase $this->assertTrue(isset($result[2])); $this->assertEquals(2, $result[2][0]->id); } + + /** + * @group DDC-1385 + */ + public function testIndexByScalarsOnly() + { + $rsm = new ResultSetMapping; + $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); + } } From e19fd756cbc5f57e696a91b8b3626747c70c0192 Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Wed, 19 Oct 2011 09:07:18 +0200 Subject: [PATCH 05/77] Better indentation for generated class --- lib/Doctrine/ORM/Tools/EntityGenerator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index 62efbac85..0792ceb91 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -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 ''; From 9c1202a76667d8e778a29d008a54c4ad3bda9c2e Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Wed, 19 Oct 2011 09:15:41 +0200 Subject: [PATCH 06/77] Added on generated class. This allow to unset many-to-one and one-to-one relations Example: $user->setGroup(null); --- lib/Doctrine/ORM/Tools/EntityGenerator.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index 0792ceb91..e8ae73aee 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; @@ -634,7 +634,7 @@ public function () foreach ($metadata->associationMappings as $associationMapping) { if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) { - if ($code = $this->_generateEntityStubMethod($metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'])) { + if ($code = $this->_generateEntityStubMethod($metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'], 'null')) { $methods[] = $code; } if ($code = $this->_generateEntityStubMethod($metadata, 'get', $associationMapping['fieldName'], $associationMapping['targetEntity'])) { @@ -707,7 +707,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 +737,7 @@ public function () '' => Inflector::camelize($fieldName), '' => $methodName, '' => $fieldName, + '' => ($defaultValue!==null?('='.$defaultValue):''), '' => $this->_getClassName($metadata) ); From 8f092812c42dfae3f0c80f504c1766812bf59443 Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Wed, 19 Oct 2011 09:17:30 +0200 Subject: [PATCH 07/77] Spaces --- lib/Doctrine/ORM/Tools/EntityGenerator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index e8ae73aee..f130908e7 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -737,7 +737,7 @@ public function () '' => Inflector::camelize($fieldName), '' => $methodName, '' => $fieldName, - '' => ($defaultValue!==null?('='.$defaultValue):''), + '' => ($defaultValue!==null?('='.$defaultValue):''), '' => $this->_getClassName($metadata) ); From be3adfb35ebb0c4139f8ee4cae7b88ff8c9cd5c9 Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Wed, 19 Oct 2011 09:25:40 +0200 Subject: [PATCH 08/77] With TO_MANY relations, class filed is instanceof ArrayCollection, instead of targetEntity class type. --- lib/Doctrine/ORM/Tools/EntityGenerator.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index f130908e7..a3a27892c 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -806,7 +806,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 . ' *'; From 596ba3d5b1e32f685f77e116524c83621573edb8 Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Wed, 19 Oct 2011 15:04:16 +0200 Subject: [PATCH 09/77] Collection inteface instead of ArrayCollection --- lib/Doctrine/ORM/Tools/EntityGenerator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index a3a27892c..7de6bdaea 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -808,7 +808,7 @@ public function () $lines[] = $this->_spaces . '/**'; if ($associationMapping['type'] & ClassMetadataInfo::TO_MANY) { - $lines[] = $this->_spaces . ' * @var \Doctrine\Common\Collections\ArrayCollection'; + $lines[] = $this->_spaces . ' * @var \Doctrine\Common\Collections\Collection'; }else{ $lines[] = $this->_spaces . ' * @var ' . $associationMapping['targetEntity']; } From fe84a61d0b24ecc9bb5ba323e56a2fb00eaf3263 Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Fri, 21 Oct 2011 09:38:37 +0200 Subject: [PATCH 10/77] Better code generation when association is nullable --- lib/Doctrine/ORM/Tools/EntityGenerator.php | 23 ++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index 7de6bdaea..bbde24346 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -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'], 'null')) { + $nullable = $this->_associationIsNullable($associationMapping); + if ($code = $this->_generateEntityStubMethod($metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'], ($nullable?'null':null))) { $methods[] = $code; } if ($code = $this->_generateEntityStubMethod($metadata, 'get', $associationMapping['fieldName'], $associationMapping['targetEntity'])) { @@ -653,6 +654,24 @@ public function () return implode("\n\n", $methods); } + private function _associationIsNullable($associationMapping) + { + if(isset($associationMapping['joinColumns'])){ + $joinColumns = $associationMapping['joinColumns']; + }else{ + //@todo thereis no way to retreive targetEntity metadata + //$targetMetadata = $this->getClassMetadata($associationMapping['targetEntity']); + //$joinColumns = $targetMetadata->associationMappings[$associationMapping["mappedBy"]]['joinColumns']; + $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) { @@ -808,7 +827,7 @@ public function () $lines[] = $this->_spaces . '/**'; if ($associationMapping['type'] & ClassMetadataInfo::TO_MANY) { - $lines[] = $this->_spaces . ' * @var \Doctrine\Common\Collections\Collection'; + $lines[] = $this->_spaces . ' * @var \Doctrine\Common\Collections\ArrayCollection'; }else{ $lines[] = $this->_spaces . ' * @var ' . $associationMapping['targetEntity']; } From d4059b88ca8048d655015682262c3b9b9976b5a1 Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Fri, 21 Oct 2011 15:30:21 +0200 Subject: [PATCH 11/77] Nullable relations, fixing join condition --- lib/Doctrine/ORM/Tools/EntityGenerator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index bbde24346..c135ecac4 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -634,7 +634,7 @@ public function () foreach ($metadata->associationMappings as $associationMapping) { if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) { - $nullable = $this->_associationIsNullable($associationMapping); + $nullable = $this->_isAssociationIsNullable($associationMapping); if ($code = $this->_generateEntityStubMethod($metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'], ($nullable?'null':null))) { $methods[] = $code; } @@ -654,7 +654,7 @@ public function () return implode("\n\n", $methods); } - private function _associationIsNullable($associationMapping) + private function _isAssociationIsNullable($associationMapping) { if(isset($associationMapping['joinColumns'])){ $joinColumns = $associationMapping['joinColumns']; @@ -665,7 +665,7 @@ public function () $joinColumns = array(); } foreach ($joinColumns as $joinColumn) { - if(!isset($joinColumn['nullable']) || !$joinColumn['nullable']){ + if(isset($joinColumn['nullable']) && !$joinColumn['nullable']){ return false; } } From 079e2b130286f52b797edbe547a1ce31815f8a80 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 22 Oct 2011 16:27:56 +0200 Subject: [PATCH 12/77] [DDC-1384] Fix for generating sql with aliases not longer than MaxIdentifierLength --- .../AbstractEntityInheritancePersister.php | 9 ++-- .../ORM/Persisters/BasicEntityPersister.php | 23 ++++++-- lib/Doctrine/ORM/Query/SqlWalker.php | 53 +++++++------------ .../ORM/Query/SelectSqlGenerationTest.php | 29 ++++++++-- 4 files changed, 67 insertions(+), 47 deletions(-) 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 0379ccf56..9fa2e3dff 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -1060,8 +1060,7 @@ class BasicEntityPersister foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { if ($columnList) $columnList .= ', '; - $columnAlias = $srcColumn . $this->_sqlAliasCounter++; - $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); + $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,11 +1179,10 @@ class BasicEntityPersister */ protected function _getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r') { - $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($class->columnNames[$field]); + $this->_rsm->addFieldResult($alias, $columnAlias, $field); return $sql . ' AS ' . $columnAlias; @@ -1500,4 +1498,19 @@ class BasicEntityPersister return (bool) $this->_conn->fetchColumn($sql, $params); } + + /** + * 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/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 188350197..6dc662493 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -229,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()) + ); } /** @@ -535,7 +539,6 @@ class SqlWalker implements TreeWalker $sqlSelectExpressions[] = $tblAlias . '.' . $discrColumn['name'] . ' AS ' . $columnAlias; - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); $this->_rsm->setDiscriminatorColumn($dqlAlias, $columnAlias); $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $discrColumn['fieldName']); @@ -556,8 +559,7 @@ class SqlWalker implements TreeWalker $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)); + $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $srcColumn, (isset($assoc['id']) && $assoc['id'] === true)); } } } @@ -574,8 +576,7 @@ class SqlWalker implements TreeWalker $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)); + $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $srcColumn, (isset($assoc['id']) && $assoc['id'] === true)); } } } @@ -1006,7 +1007,6 @@ class SqlWalker implements TreeWalker $columnAlias = $this->getSQLColumnAlias($columnName); $sql .= $sqlTableAlias . '.' . $columnName . ' AS ' . $columnAlias; - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); if ( ! $hidden) { $this->_rsm->addScalarResult($columnAlias, $resultAlias); @@ -1018,12 +1018,10 @@ class SqlWalker implements TreeWalker $resultAlias = $selectExpression->fieldIdentificationVariable; } - $columnAlias = 'sclr' . $this->_aliasCounter++; + $columnAlias = $this->getSQLColumnAlias('sclr'); $sql .= $this->walkAggregateExpression($expr) . ' AS ' . $columnAlias; $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - if ( ! $hidden) { $this->_rsm->addScalarResult($columnAlias, $resultAlias); } @@ -1034,12 +1032,10 @@ class SqlWalker implements TreeWalker $resultAlias = $selectExpression->fieldIdentificationVariable; } - $columnAlias = 'sclr' . $this->_aliasCounter++; + $columnAlias = $this->getSQLColumnAlias('sclr'); $sql .= '(' . $this->walkSubselect($expr) . ') AS '.$columnAlias; $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - if ( ! $hidden) { $this->_rsm->addScalarResult($columnAlias, $resultAlias); } @@ -1050,12 +1046,10 @@ class SqlWalker implements TreeWalker $resultAlias = $selectExpression->fieldIdentificationVariable; } - $columnAlias = 'sclr' . $this->_aliasCounter++; + $columnAlias = $this->getSQLColumnAlias('sclr'); $sql .= $this->walkFunction($expr) . ' AS ' . $columnAlias; $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - if ( ! $hidden) { $this->_rsm->addScalarResult($columnAlias, $resultAlias); } @@ -1072,18 +1066,15 @@ class SqlWalker implements TreeWalker $resultAlias = $selectExpression->fieldIdentificationVariable; } - $columnAlias = 'sclr' . $this->_aliasCounter++; - + $columnAlias = $this->getSQLColumnAlias('sclr'); 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); } @@ -1099,14 +1090,11 @@ class SqlWalker implements TreeWalker $resultAlias = $selectExpression->fieldIdentificationVariable; } - $columnAlias = 'sclr' . $this->_aliasCounter++; - + $columnAlias = $this->getSQLColumnAlias('sclr'); $sql .= $this->walkCaseExpression($expr) . ' AS ' . $columnAlias; - + $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - if ( ! $hidden) { $this->_rsm->addScalarResult($columnAlias, $resultAlias); } @@ -1146,8 +1134,6 @@ class SqlWalker implements TreeWalker $sql .= $sqlTableAlias . '.' . $class->getQuotedColumnName($fieldName, $this->_platform) . ' AS ' . $columnAlias; - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); - $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); } @@ -1171,7 +1157,6 @@ class SqlWalker implements TreeWalker $sql .= $sqlTableAlias . '.' . $subClass->getQuotedColumnName($fieldName, $this->_platform) . ' AS ' . $columnAlias; - $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); } @@ -1185,7 +1170,7 @@ class SqlWalker implements TreeWalker $columnAlias = $this->getSQLColumnAlias($srcColumn); $sql .= $sqlTableAlias . '.' . $srcColumn . ' AS ' . $columnAlias; - $this->_rsm->addMetaResult($dqlAlias, $this->_platform->getSQLResultCasing($columnAlias), $srcColumn); + $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $srcColumn); } } } @@ -1312,7 +1297,7 @@ class SqlWalker implements TreeWalker $alias = $simpleSelectExpression->fieldIdentificationVariable; } - $columnAlias = 'sclr' . $this->_aliasCounter++; + $columnAlias = $this->getSQLColumnAlias('sclr'); $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; $this->_scalarResultAliasMap[$alias] = $columnAlias; } else if ($expr instanceof AST\Functions\FunctionNode) { @@ -1322,7 +1307,7 @@ class SqlWalker implements TreeWalker $alias = $simpleSelectExpression->fieldIdentificationVariable; } - $columnAlias = 'sclr' . $this->_aliasCounter++; + $columnAlias = $this->getSQLColumnAlias('sclr'); $sql .= $this->walkFunction($expr) . ' AS ' . $columnAlias; $this->_scalarResultAliasMap[$alias] = $columnAlias; } else if ( @@ -1337,7 +1322,7 @@ class SqlWalker implements TreeWalker $alias = $simpleSelectExpression->fieldIdentificationVariable; } - $columnAlias = 'sclr' . $this->_aliasCounter++; + $columnAlias = $this->getSQLColumnAlias('sclr'); $sql .= $this->walkSimpleArithmeticExpression($expr) . ' AS ' . $columnAlias; $this->_scalarResultAliasMap[$alias] = $columnAlias; } else if ( @@ -1352,7 +1337,7 @@ class SqlWalker implements TreeWalker $alias = $simpleSelectExpression->fieldIdentificationVariable; } - $columnAlias = 'sclr' . $this->_aliasCounter++; + $columnAlias = $this->getSQLColumnAlias('sclr'); $sql .= $this->walkCaseExpression($expr) . ' AS ' . $columnAlias; $this->_scalarResultAliasMap[$alias] = $columnAlias; diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 53002a8fc..e3ea9c004 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -729,12 +729,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); @@ -877,7 +877,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) ); @@ -1251,6 +1251,17 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase array(Query::HINT_FORCE_PARTIAL_LOAD => false) ); } + + /** + * @group DDC-1384 + */ + function testAliasDoesNotExceedPlatformDefinedLength() + { + $this->assertSqlGeneration( + 'SELECT m FROM ' . __NAMESPACE__ . '\\DDC1384Model m', + "SELECT d0_.aVeryLongIdentifierThatShouldBeShortenedByTheSQLWalker_fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo AS fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0 FROM DDC1384Model d0_" + ); + } } @@ -1281,3 +1292,15 @@ class MyAbsFunction extends \Doctrine\ORM\Query\AST\Functions\FunctionNode $parser->match(\Doctrine\ORM\Query\Lexer::T_CLOSE_PARENTHESIS); } } +/** + * @Entity + */ +class DDC1384Model +{ + /** + * @Id + * @Column(type="integer") + * @GeneratedValue + */ + protected $aVeryLongIdentifierThatShouldBeShortenedByTheSQLWalker_fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo; +} From 66e2a9260e1306d7ffc6bc952a0a890f30916ae4 Mon Sep 17 00:00:00 2001 From: everzet Date: Sun, 23 Oct 2011 18:39:16 +0300 Subject: [PATCH 13/77] added PreFlush lifetime event and lifecycle callback --- lib/Doctrine/ORM/Event/PreFlushEventArgs.php | 56 +++++++++++++++++++ lib/Doctrine/ORM/Events.php | 7 +++ .../ORM/Mapping/Driver/AnnotationDriver.php | 4 ++ .../Mapping/Driver/DoctrineAnnotations.php | 6 ++ lib/Doctrine/ORM/UnitOfWork.php | 35 ++++++++++++ 5 files changed, 108 insertions(+) create mode 100644 lib/Doctrine/ORM/Event/PreFlushEventArgs.php diff --git a/lib/Doctrine/ORM/Event/PreFlushEventArgs.php b/lib/Doctrine/ORM/Event/PreFlushEventArgs.php new file mode 100644 index 000000000..8ed39b6cb --- /dev/null +++ b/lib/Doctrine/ORM/Event/PreFlushEventArgs.php @@ -0,0 +1,56 @@ +. +*/ + +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; + + //private $_entitiesToPersist = array(); + //private $_entitiesToRemove = array(); + + 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/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 36e3dcb01..b09cabc95 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -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); + } } } } 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/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 4366250dd..d8b640c47 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -259,6 +259,41 @@ class UnitOfWork implements PropertyChangedListener */ public function commit($entity = null) { + // Run preFlush lifecycle callback for new entities + foreach ($this->entityInsertions as $classEntity) { + $class = $this->em->getClassMetadata(get_class($classEntity)); + + // Skip class if instances are read-only + if ($class->isReadOnly) { + continue; + } + + if (isset($class->lifecycleCallbacks[Events::preFlush])) { + $class->invokeLifecycleCallbacks(Events::preFlush, $classEntity); + } + } + + // Run preFlush lifecycle callback for persisted entities + foreach ($this->identityMap as $className => $classEntities) { + $class = $this->em->getClassMetadata($className); + + // Skip class if instances are read-only + if ($class->isReadOnly) { + continue; + } + + if (isset($class->lifecycleCallbacks[Events::preFlush])) { + foreach ($classEntities as $classEntity) { + $class->invokeLifecycleCallbacks(Events::preFlush, $classEntity); + } + } + } + + // 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(); From 20ed8869e43cd7f583a75339bca470212a6f199d Mon Sep 17 00:00:00 2001 From: everzet Date: Sun, 23 Oct 2011 18:39:53 +0300 Subject: [PATCH 14/77] added test for PreFlush lifetime event --- .../ORM/Functional/LifecycleCallbackTest.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) 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; + } } /** From 91d8829c431b332d357ee8ce95e2f374d45b028e Mon Sep 17 00:00:00 2001 From: everzet Date: Sun, 23 Oct 2011 18:50:24 +0300 Subject: [PATCH 15/77] removed non-used code --- lib/Doctrine/ORM/Event/PreFlushEventArgs.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/Doctrine/ORM/Event/PreFlushEventArgs.php b/lib/Doctrine/ORM/Event/PreFlushEventArgs.php index 8ed39b6cb..b86967a72 100644 --- a/lib/Doctrine/ORM/Event/PreFlushEventArgs.php +++ b/lib/Doctrine/ORM/Event/PreFlushEventArgs.php @@ -38,9 +38,6 @@ class PreFlushEventArgs extends \Doctrine\Common\EventArgs */ private $_em; - //private $_entitiesToPersist = array(); - //private $_entitiesToRemove = array(); - public function __construct($em) { $this->_em = $em; From 5f80b575541d53489f651f9d50aa44db5bdb49df Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Mon, 24 Oct 2011 10:19:01 +0200 Subject: [PATCH 16/77] Improoved coding standards --- lib/Doctrine/ORM/Tools/EntityGenerator.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index c135ecac4..1542f6cf0 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -635,7 +635,7 @@ public function () foreach ($metadata->associationMappings as $associationMapping) { if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) { $nullable = $this->_isAssociationIsNullable($associationMapping); - if ($code = $this->_generateEntityStubMethod($metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'], ($nullable?'null':null))) { + if ($code = $this->_generateEntityStubMethod($metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'], ($nullable ? 'null' : null))) { $methods[] = $code; } if ($code = $this->_generateEntityStubMethod($metadata, 'get', $associationMapping['fieldName'], $associationMapping['targetEntity'])) { @@ -656,16 +656,14 @@ public function () private function _isAssociationIsNullable($associationMapping) { - if(isset($associationMapping['joinColumns'])){ + if (isset($associationMapping['joinColumns'])) { $joinColumns = $associationMapping['joinColumns']; - }else{ + } else { //@todo thereis no way to retreive targetEntity metadata - //$targetMetadata = $this->getClassMetadata($associationMapping['targetEntity']); - //$joinColumns = $targetMetadata->associationMappings[$associationMapping["mappedBy"]]['joinColumns']; $joinColumns = array(); } foreach ($joinColumns as $joinColumn) { - if(isset($joinColumn['nullable']) && !$joinColumn['nullable']){ + if(isset($joinColumn['nullable']) && !$joinColumn['nullable']) { return false; } } @@ -756,7 +754,7 @@ public function () '' => Inflector::camelize($fieldName), '' => $methodName, '' => $fieldName, - '' => ($defaultValue!==null?('='.$defaultValue):''), + '' => (($defaultValue !== null ) ? ('='.$defaultValue) : ''), '' => $this->_getClassName($metadata) ); @@ -825,10 +823,10 @@ public function () { $lines = array(); $lines[] = $this->_spaces . '/**'; - + if ($associationMapping['type'] & ClassMetadataInfo::TO_MANY) { $lines[] = $this->_spaces . ' * @var \Doctrine\Common\Collections\ArrayCollection'; - }else{ + } else { $lines[] = $this->_spaces . ' * @var ' . $associationMapping['targetEntity']; } From f5330741ac1bff9e015172e2ec9c2a3f019cd84b Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Mon, 24 Oct 2011 10:59:50 +0200 Subject: [PATCH 17/77] Coding standards --- lib/Doctrine/ORM/Tools/EntityGenerator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index 1542f6cf0..b7522380f 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -634,8 +634,8 @@ public function () foreach ($metadata->associationMappings as $associationMapping) { if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) { - $nullable = $this->_isAssociationIsNullable($associationMapping); - if ($code = $this->_generateEntityStubMethod($metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'], ($nullable ? 'null' : null))) { + $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'])) { From 7efd615b8c6d6658b05740c923170ef8a24dbba7 Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Mon, 24 Oct 2011 12:00:11 +0200 Subject: [PATCH 18/77] Coding standards --- lib/Doctrine/ORM/Tools/EntityGenerator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index 1542f6cf0..b1ba714d8 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -634,8 +634,8 @@ public function () foreach ($metadata->associationMappings as $associationMapping) { if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) { - $nullable = $this->_isAssociationIsNullable($associationMapping); - if ($code = $this->_generateEntityStubMethod($metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'], ($nullable ? 'null' : null))) { + $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'])) { @@ -754,7 +754,7 @@ public function () '' => Inflector::camelize($fieldName), '' => $methodName, '' => $fieldName, - '' => (($defaultValue !== null ) ? ('='.$defaultValue) : ''), + '' => ($defaultValue !== null ) ? ('='.$defaultValue) : '', '' => $this->_getClassName($metadata) ); From 0a5a23628f74f3203d2e659939b4aee39fa4c462 Mon Sep 17 00:00:00 2001 From: Lukas Kahwe Smith Date: Tue, 25 Oct 2011 23:21:39 +0200 Subject: [PATCH 19/77] added EntityRepository::getClassName() to fullfill the ObjectRepository interface see https://github.com/doctrine/common/pull/70 --- lib/Doctrine/ORM/EntityRepository.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 +} From 66d2b9e0fba22e6e6b524548ee8a9c2b6df76d08 Mon Sep 17 00:00:00 2001 From: Thiago Festa Date: Fri, 28 Oct 2011 17:54:15 -0200 Subject: [PATCH 20/77] The ProxyFactory was redeclaring methods serialize and unserialize on the cache file on some OSs. --- lib/Doctrine/ORM/Proxy/ProxyFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index 490c3a119..f9ed3640a 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -167,7 +167,7 @@ class ProxyFactory 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")) || $class->reflClass->getName() != $method->class) { continue; } From 0ec2cc557f51d6240396689e36101f62d84d2a38 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Sat, 29 Oct 2011 02:00:35 -0200 Subject: [PATCH 21/77] Implemented support to entities with association marked as @Id support in many situations. Fixed DDC-1435. --- .../ORM/Mapping/ClassMetadataInfo.php | 72 ++++++++++++++----- lib/Doctrine/ORM/Query/SqlWalker.php | 20 +++--- .../Tests/Models/DDC117/DDC117Article.php | 1 + .../ORM/Query/SelectSqlGenerationTest.php | 11 +++ 4 files changed, 75 insertions(+), 29 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 25a3350d2..10f7bc4b6 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -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. */ @@ -1904,6 +1904,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 +1950,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/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 6e6f80ce6..8939d198e 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -248,10 +248,9 @@ class SqlWalker implements TreeWalker $sql .= 'JOIN ' . $parentClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON '; $first = true; - foreach ($class->identifier as $idField) { + foreach ($class->getQuotedIdentifierColumnNames($this->_platform) as $columnName) { if ($first) $first = false; else $sql .= ' AND '; - $columnName = $class->getQuotedColumnName($idField, $this->_platform); $sql .= $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; } } @@ -264,10 +263,9 @@ class SqlWalker implements TreeWalker $sql .= ' LEFT JOIN ' . $subClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON '; $first = true; - foreach ($class->identifier as $idField) { + foreach ($class->getQuotedIdentifierColumnNames($this->_platform) as $columnName) { if ($first) $first = false; else $sql .= ' AND '; - $columnName = $class->getQuotedColumnName($idField, $this->_platform); $sql .= $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; } } @@ -1346,8 +1344,8 @@ class SqlWalker implements TreeWalker $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); @@ -1634,12 +1632,11 @@ class SqlWalker implements TreeWalker $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']); @@ -1684,12 +1681,11 @@ class SqlWalker implements TreeWalker $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 . ' = ?'; } } 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/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 8b501a50e..6589890fc 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -1268,6 +1268,17 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase array(Query::HINT_FORCE_PARTIAL_LOAD => false) ); } + + /** + * @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)" + ); + } } From e39bfced4a584ca967fce079385df3d57e176d54 Mon Sep 17 00:00:00 2001 From: Adrien BRAULT Date: Sat, 29 Oct 2011 13:40:01 +0200 Subject: [PATCH 22/77] Fix iterate method doc return type --- lib/Doctrine/ORM/AbstractQuery.php | 2 +- lib/Doctrine/ORM/Query.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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) { From fb55e8094b408d1943f113e04f9eb4c0ac135390 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 29 Oct 2011 18:48:34 +0200 Subject: [PATCH 23/77] DDC-1384 - Made tests case-insenstive to work with latest change regarding column sizes --- tests/Doctrine/Tests/ORM/Functional/Ticket/DDC448Test.php | 5 ++++- tests/Doctrine/Tests/ORM/Functional/Ticket/DDC493Test.php | 5 ++++- tests/Doctrine/Tests/ORM/Functional/Ticket/DDC513Test.php | 5 ++++- tests/Doctrine/Tests/ORM/Functional/Ticket/DDC698Test.php | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) 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) + ); } } From 50e028212d1e30cc4d32b95eeee4480d2446644c Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 29 Oct 2011 20:42:44 +0200 Subject: [PATCH 24/77] DDC-1384 - Fix a bunch of Oracle test failures --- .../ORM/Query/ResultSetMappingBuilder.php | 6 +- .../ORM/Functional/CustomTreeWalkersTest.php | 26 ++-- .../Tests/ORM/Functional/NativeQueryTest.php | 40 +++--- .../ORM/Functional/Ticket/DDC1225Test.php | 10 +- .../ORM/Functional/Ticket/DDC331Test.php | 17 +-- .../ORM/Functional/Ticket/DDC719Test.php | 11 +- .../ORM/Query/SelectSqlGenerationTest.php | 130 ++++++++++-------- 7 files changed, 127 insertions(+), 113 deletions(-) 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/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/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/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/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/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/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index e3ea9c004..c5a41e7c7 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 */ @@ -686,7 +686,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( @@ -694,7 +694,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 */ @@ -931,7 +931,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( @@ -939,7 +939,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase 'SELECT NULLIF(c0_.id, c0_.name) AS sclr0 FROM cms_groups c0_' ); } - + public function testCaseContainingCoalesce() { $this->assertSqlGeneration( @@ -1009,244 +1009,244 @@ 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 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', 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 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', 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 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", array(Query::HINT_FORCE_PARTIAL_LOAD => false) ); @@ -1255,13 +1255,25 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase /** * @group DDC-1384 */ - function testAliasDoesNotExceedPlatformDefinedLength() + 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' + ); + } } @@ -1288,7 +1300,7 @@ 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); } } From 30731e0727d05248e9da9dc3ef7956647637b3b2 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 29 Oct 2011 23:58:09 +0200 Subject: [PATCH 25/77] DDC-1384 - Fix all tests on Oracle --- lib/Doctrine/ORM/Tools/SchemaTool.php | 18 ++-- lib/Doctrine/ORM/Tools/ToolsException.php | 27 +++++ .../Tests/Models/Legacy/LegacyUser.php | 2 +- .../Models/Legacy/LegacyUserReference.php | 4 +- .../Tests/ORM/Functional/ReadOnlyTest.php | 8 +- .../ORM/Functional/Ticket/DDC1040Test.php | 8 +- .../ORM/Functional/Ticket/DDC1209Test.php | 2 +- .../ORM/Functional/Ticket/DDC1228Test.php | 44 ++++---- .../ORM/Functional/Ticket/DDC1238Test.php | 32 +++--- .../ORM/Functional/Ticket/DDC1335Test.php | 102 +++++++++--------- 10 files changed, 139 insertions(+), 108 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index 77106ec7d..568a960a4 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 @@ -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/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/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/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/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/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/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 From 1bbec8dd33881aaf54a58d67ac3ae739a6f39f19 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 30 Oct 2011 00:25:49 +0200 Subject: [PATCH 26/77] Clarify error message --- tests/Doctrine/Tests/ORM/Functional/Ticket/DDC949Test.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 2c036b3185c4da38fb447b9d576e4219fe6af835 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 30 Oct 2011 08:25:19 +0100 Subject: [PATCH 27/77] Fix glitch in testsuites tearDown() --- .../ORM/Functional/EntityRepositoryTest.php | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) 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"); } - + } From 3cfa479c01e927364edd8d5f96b86f602341d6b2 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Sun, 30 Oct 2011 15:46:07 -0200 Subject: [PATCH 28/77] Micro optimization in computeChangeSet when using readOnly entities. --- lib/Doctrine/ORM/UnitOfWork.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index e499d3c5f..6add3bd6b 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -471,19 +471,21 @@ 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)); + } + $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 From c246c6b28b0569bed6ffd259df2a87a2f8ecf08f Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Sun, 30 Oct 2011 16:07:43 -0200 Subject: [PATCH 29/77] AssignedGenerator optimization. --- lib/Doctrine/ORM/Id/AssignedGenerator.php | 54 ++++++++--------------- 1 file changed, 19 insertions(+), 35 deletions(-) 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; } } From d444f0e06b6d588d62bdf3493a282cf61bfcd956 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Sun, 30 Oct 2011 16:22:45 -0200 Subject: [PATCH 30/77] Micro optimization in SqlWalker. --- lib/Doctrine/ORM/Query/SqlWalker.php | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 45837bf1f..d90dc28f2 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -980,17 +980,13 @@ class SqlWalker implements TreeWalker $qComp = $this->_queryComponents[$dqlAlias]; $class = $qComp['metadata']; - if ( ! $selectExpression->fieldIdentificationVariable) { - $resultAlias = $fieldName; - } else { - $resultAlias = $selectExpression->fieldIdentificationVariable; - } + $resultAlias = ( ! $selectExpression->fieldIdentificationVariable) + ? $fieldName + : $selectExpression->fieldIdentificationVariable; - if ($class->isInheritanceTypeJoined()) { - $tableName = $this->_em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName); - } else { - $tableName = $class->getTableName(); - } + $tableName = ($class->isInheritanceTypeJoined()) + ? $this->_em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName) + : $class->getTableName(); $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); $columnName = $class->getQuotedColumnName($fieldName, $this->_platform); From 11f82bd41f021560bcbb4d611c51d2c4b71715fb Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 31 Oct 2011 20:49:28 +0100 Subject: [PATCH 31/77] DDC-1439 - Fix validate mapping some more --- lib/Doctrine/ORM/Tools/SchemaValidator.php | 306 +++++++++--------- .../Tests/ORM/Tools/SchemaValidatorTest.php | 84 +++++ 2 files changed, 245 insertions(+), 145 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/SchemaValidator.php b/lib/Doctrine/ORM/Tools/SchemaValidator.php index a7f8e3a1c..2ddd0bf66 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,165 @@ 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(), $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(), $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'])) { + $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(), $assoc['joinColumns'])) . + "' 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. * 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 From 95193ab5f84a8f2a82c969ae784a7bd049fcefee Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 31 Oct 2011 21:17:01 +0100 Subject: [PATCH 32/77] DDC-1399 - Fix extra lazy collections when inner collection contains values but persistent collection is marked not dirty because of flush() --- lib/Doctrine/ORM/PersistentCollection.php | 46 +++++++++---------- .../Functional/ExtraLazyCollectionTest.php | 32 +++++++++++-- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index 82d616686..9840bead2 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. */ diff --git a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php index 4bf010602..43b45650f 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,28 @@ 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)); + } + private function loadFixture() { $user1 = new \Doctrine\Tests\Models\CMS\CmsUser(); @@ -364,7 +386,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->persist($article1); $this->_em->persist($article2); - + $this->_em->flush(); $this->_em->clear(); From 1dc5b7fba4496274287ca87b95c2b6b3ef27fbd8 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 31 Oct 2011 21:34:22 +0100 Subject: [PATCH 33/77] DDC-1462 - Fix bug in slice when calling on a dirty collection that is marked extra lazy --- lib/Doctrine/ORM/PersistentCollection.php | 5 ++++- .../Functional/ExtraLazyCollectionTest.php | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index 9840bead2..b64cacfdc 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -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/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php index 43b45650f..351c1e25b 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -326,6 +326,27 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $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(); From 3994b80aa4c31e7ca4253f1fef1c2d9a72789c3f Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 31 Oct 2011 21:36:55 +0100 Subject: [PATCH 34/77] Fix TODO: Inner join when all join columns are NOT nullable. --- .../ORM/Persisters/BasicEntityPersister.php | 23 +++++++++++++++++-- .../Functional/OneToOneEagerLoadingTest.php | 3 ++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 0379ccf56..78c79c283 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -1002,10 +1002,10 @@ class BasicEntityPersister } } - $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) { @@ -1020,7 +1020,8 @@ class BasicEntityPersister } else { $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']); $owningAssoc = $eagerEntity->getAssociationMapping($assoc['mappedBy']); - + + $this->_selectJoinSql .= $this->getJoinSQLForJoinColumns($owningAssoc['joinColumns']); $this->_selectJoinSql .= ' ' . $eagerEntity->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) . ' ON '; @@ -1500,4 +1501,22 @@ 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 '; + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php index 044a17381..7b87046f9 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -130,6 +130,7 @@ class Train /** * Owning side * @OneToOne(targetEntity="TrainDriver", inversedBy="train", fetch="EAGER", cascade={"persist"}) + * @JoinColumn(nullable=true) */ public $driver; /** @@ -195,4 +196,4 @@ class Waggon { $this->train = $train; } -} \ No newline at end of file +} From 0f938b8c1d37402a80c94c0b16c8d5020f8f314d Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 31 Oct 2011 21:53:46 +0100 Subject: [PATCH 35/77] Added tests for inner join generation with eager loading --- .../Functional/OneToOneEagerLoadingTest.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php index 7b87046f9..f8760c6f6 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -115,6 +115,34 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $waggon->train); $this->assertNotNull($waggon->train); } + + public function testEagerLoadWithNullableColumnsGeneratesLeftJoin() + { + $train = new Train(); + $this->_em->persist($train); + $this->_em->flush(); + $this->_em->clear(); + + $train = $this->_em->find(get_class($train), $train->id); + $this->assertEquals( + $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery]['sql'], + "SELECT t0.id AS id1, t0.driver_id AS driver_id2, t3.id AS id4, t3.name AS name5 FROM Train t0 LEFT JOIN TrainDriver t3 ON t0.driver_id = t3.id WHERE t0.id = ?" + ); + } + + public function testEagerLoadWithNonNullableColumnsGeneratesInnerJoin() + { + $waggon = new Waggon(); + $this->_em->persist($waggon); + $this->_em->flush(); + $this->_em->clear(); + + $waggon = $this->_em->find(get_class($waggon), $waggon->id); + $this->assertEquals( + $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery]['sql'], + "SELECT t0.id AS id1, t0.train_id AS train_id2, t3.id AS id4, t3.driver_id AS driver_id5 FROM Waggon t0 INNER JOIN Train t3 ON t0.train_id = t3.id WHERE t0.id = ?" + ); + } } /** From 22b3b46b61fa0cf647129fe2b52dd06549475dab Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 31 Oct 2011 22:08:40 +0100 Subject: [PATCH 36/77] Removed unnecessary spaces in generated SQL --- lib/Doctrine/ORM/Persisters/BasicEntityPersister.php | 12 ++++++------ .../ORM/Functional/OneToOneEagerLoadingTest.php | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 78c79c283..b747155b3 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -1005,7 +1005,7 @@ class BasicEntityPersister $first = true; if ($assoc['isOwningSide']) { - $this->_selectJoinSql .= $this->getJoinSQLForJoinColumns($assoc['joinColumns']); + $this->_selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($assoc['joinColumns']); $this->_selectJoinSql .= ' ' . $eagerEntity->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON '; foreach ($assoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) { @@ -1014,14 +1014,14 @@ class BasicEntityPersister } $this->_selectJoinSql .= $this->_getSQLTableAlias($assoc['sourceEntity']) . '.' . $sourceCol . ' = ' - . $this->_getSQLTableAlias($assoc['targetEntity'], $assocAlias) . '.' . $targetCol . ' '; + . $this->_getSQLTableAlias($assoc['targetEntity'], $assocAlias) . '.' . $targetCol; $first = false; } } else { $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']); $owningAssoc = $eagerEntity->getAssociationMapping($assoc['mappedBy']); - $this->_selectJoinSql .= $this->getJoinSQLForJoinColumns($owningAssoc['joinColumns']); + $this->_selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($owningAssoc['joinColumns']); $this->_selectJoinSql .= ' ' . $eagerEntity->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) . ' ON '; @@ -1031,7 +1031,7 @@ class BasicEntityPersister } $this->_selectJoinSql .= $this->_getSQLTableAlias($owningAssoc['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = ' - . $this->_getSQLTableAlias($owningAssoc['targetEntity']) . '.' . $targetCol . ' '; + . $this->_getSQLTableAlias($owningAssoc['targetEntity']) . '.' . $targetCol; $first = false; } } @@ -1513,10 +1513,10 @@ class BasicEntityPersister // 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 'LEFT JOIN'; } } - return ' INNER JOIN '; + return 'INNER JOIN'; } } diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php index f8760c6f6..5361aad45 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -125,8 +125,8 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $train = $this->_em->find(get_class($train), $train->id); $this->assertEquals( - $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery]['sql'], - "SELECT t0.id AS id1, t0.driver_id AS driver_id2, t3.id AS id4, t3.name AS name5 FROM Train t0 LEFT JOIN TrainDriver t3 ON t0.driver_id = t3.id WHERE t0.id = ?" + "SELECT t0.id AS id1, t0.driver_id AS driver_id2, t3.id AS id4, t3.name AS name5 FROM Train t0 LEFT JOIN TrainDriver t3 ON t0.driver_id = t3.id WHERE t0.id = ?", + $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery]['sql'] ); } @@ -139,8 +139,8 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $waggon = $this->_em->find(get_class($waggon), $waggon->id); $this->assertEquals( - $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery]['sql'], - "SELECT t0.id AS id1, t0.train_id AS train_id2, t3.id AS id4, t3.driver_id AS driver_id5 FROM Waggon t0 INNER JOIN Train t3 ON t0.train_id = t3.id WHERE t0.id = ?" + "SELECT t0.id AS id1, t0.train_id AS train_id2, t3.id AS id4, t3.driver_id AS driver_id5 FROM Waggon t0 INNER JOIN Train t3 ON t0.train_id = t3.id WHERE t0.id = ?", + $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery]['sql'] ); } } From 231d84b625f4374ed196eb53d46b50fc6f7a12d3 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 31 Oct 2011 22:14:46 +0100 Subject: [PATCH 37/77] Fix xml fix again --- lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index b676ca8da..fd6bf081a 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)); @@ -327,6 +327,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 +434,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); From e31e164896dc391b7e5ec03cdfbc97351e5cea93 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 31 Oct 2011 23:12:52 +0100 Subject: [PATCH 38/77] DBAL-1420 - Use safe mode for schema validation. Dropping stuff isnt necessary here. --- lib/Doctrine/ORM/Tools/SchemaValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Tools/SchemaValidator.php b/lib/Doctrine/ORM/Tools/SchemaValidator.php index 2ddd0bf66..2c0bd94ff 100644 --- a/lib/Doctrine/ORM/Tools/SchemaValidator.php +++ b/lib/Doctrine/ORM/Tools/SchemaValidator.php @@ -246,6 +246,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); } } From c965d231b1d32aa9ee380e915a1bf8cf5ebf48ab Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 31 Oct 2011 23:21:11 +0100 Subject: [PATCH 39/77] Rename method and refactor code a bit --- lib/Doctrine/ORM/Tools/SchemaTool.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index 568a960a4..e052c69d7 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -145,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) { @@ -177,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 */ @@ -267,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; From 4b316ec54fac9e95324ba622364427cc51a3b5fb Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 31 Oct 2011 23:35:41 +0100 Subject: [PATCH 40/77] DDC-1389 - Add validation for empty discriminator map values --- .../ORM/Internal/Hydration/HydrationException.php | 11 ++++++++++- .../ORM/Internal/Hydration/ObjectHydrator.php | 5 +++++ .../ORM/Internal/Hydration/SimpleObjectHydrator.php | 5 +++++ 3 files changed, 20 insertions(+), 1 deletion(-) 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 1bd16164b..3f787694b 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -202,6 +202,11 @@ class ObjectHydrator extends AbstractHydrator $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]); } diff --git a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php index 34d8e31b1..e9f6395f9 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php @@ -89,8 +89,13 @@ class SimpleObjectHydrator extends AbstractHydrator $entityName = $this->class->name; } else { $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])) { From 98e3e0447720121caa65ca77d6c218f9c41370fa Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 1 Nov 2011 15:05:38 +0100 Subject: [PATCH 41/77] Add autoload section to composer.json --- composer.json | 3 +++ 1 file changed, 3 insertions(+) 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/" } } } From afb8d63fcb262d1e23560425cd2066d785f98791 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 1 Nov 2011 16:42:03 +0100 Subject: [PATCH 42/77] Fixed some AbstractHydrator docblocks --- .../ORM/Internal/Hydration/AbstractHydrator.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php index 5899a69ca..61a0f48fa 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php @@ -158,11 +158,12 @@ abstract class AbstractHydrator /** * 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 @@ -203,7 +204,7 @@ abstract class AbstractHydrator $cache[$key]['isIdentifier'] = isset($this->_rsm->isIdentifierColumn[$cache[$key]['dqlAlias']][$key]); } } - + if (isset($cache[$key]['isScalar'])) { $rowData['scalars'][$cache[$key]['fieldName']] = $value; continue; @@ -221,7 +222,7 @@ abstract class AbstractHydrator } continue; } - + // in an inheritance hierachy the same field could be defined several times. // We overwrite this value so long we dont have a non-null value, that value we keep. // Per definition it cannot be that a field is defined several times and has several values. @@ -241,6 +242,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 @@ -277,7 +279,7 @@ abstract class AbstractHydrator $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; } } - + $fieldName = $cache[$key]['fieldName']; if (isset($cache[$key]['isScalar'])) { @@ -292,7 +294,7 @@ abstract class AbstractHydrator return $rowData; } - + protected function registerManaged($class, $entity, $data) { if ($class->isIdentifierComposite) { From d532de9da3e1a413302540a407d6e73da8cdf39e Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 1 Nov 2011 17:43:23 +0100 Subject: [PATCH 43/77] Fix docblocks ObjectHydratorTest --- .../ORM/Hydration/ObjectHydratorTest.php | 58 ++++++------------- 1 file changed, 19 insertions(+), 39 deletions(-) diff --git a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php index 65cd57c95..57c297cf0 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php @@ -16,7 +16,7 @@ require_once __DIR__ . '/../../TestInit.php'; class ObjectHydratorTest extends HydrationTestCase { /** - * Select u.id, u.name from \Doctrine\Tests\Models\CMS\CmsUser u + * SELECT PARTIAL u.{id,name} FROM \Doctrine\Tests\Models\CMS\CmsUser u */ public function testSimpleEntityQuery() { @@ -54,6 +54,7 @@ class ObjectHydratorTest extends HydrationTestCase /** * @group DDC-644 + * SELECT PARTIAL u.{id,name} FROM \Doctrine\Tests\Models\CMS\CmsUser u */ public function testSkipUnknownColumns() { @@ -80,7 +81,8 @@ class ObjectHydratorTest extends HydrationTestCase } /** - * Select u.id, u.name from \Doctrine\Tests\Models\CMS\CmsUser u + * SELECT PARTIAL u.{id,name}, PARTIAL a.{id,topic} + * FROM \Doctrine\Tests\Models\CMS\CmsUser u, \Doctrine\Tests\Models\CMS\CmsArticle a */ public function testSimpleMultipleRootEntityQuery() { @@ -115,7 +117,7 @@ class ObjectHydratorTest extends HydrationTestCase $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]); @@ -132,7 +134,7 @@ class ObjectHydratorTest extends HydrationTestCase } /** - * Select p from \Doctrine\Tests\Models\ECommerce\ECommerceProduct p + * SELECT p FROM \Doctrine\Tests\Models\ECommerce\ECommerceProduct p */ public function testCreatesProxyForLazyLoadingWithForeignKeys() { @@ -177,11 +179,7 @@ class ObjectHydratorTest extends HydrationTestCase } /** - * 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 u, p, upper(u.name) nameUpper from User u JOIN u.phonenumbers p */ public function testMixedQueryFetchJoin() { @@ -251,11 +249,8 @@ class ObjectHydratorTest extends HydrationTestCase } /** - * 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 u, count(p.phonenumber) numPhones from User u + * join u.phonenumbers p group by u.id */ public function testMixedQueryNormalJoin() { @@ -298,11 +293,8 @@ class ObjectHydratorTest extends HydrationTestCase } /** - * select u.id, u.status, upper(u.name) nameUpper from User u index by u.id + * select u, p, upper(u.name) nameUpper from User u index by u.id * join u.phonenumbers p indexby p.phonenumber - * = - * select u.id, u.status, upper(u.name) as p__0 from USERS u - * INNER JOIN PHONENUMBERS p ON u.id = p.user_id */ public function testMixedQueryFetchJoinCustomIndex() { @@ -373,15 +365,10 @@ class ObjectHydratorTest extends HydrationTestCase } /** - * select u.id, u.status, p.phonenumber, upper(u.name) nameUpper, a.id, a.topic + * select u, p, upper(u.name) nameUpper, a * from User u * join u.phonenumbers p * join u.articles a - * = - * select u.id, u.status, p.phonenumber, upper(u.name) as u__0, a.id, a.topic - * from USERS u - * inner join PHONENUMBERS p ON u.id = p.user_id - * inner join ARTICLES a ON u.id = a.user_id */ public function testMixedQueryMultipleFetchJoin() { @@ -484,19 +471,12 @@ class ObjectHydratorTest extends HydrationTestCase } /** - * select u.id, u.status, p.phonenumber, upper(u.name) nameUpper, a.id, a.topic, + * select u, p, upper(u.name) nameUpper, a, c * c.id, c.topic * from User u * join u.phonenumbers p * join u.articles a * left join a.comments c - * = - * select u.id, u.status, p.phonenumber, upper(u.name) as u__0, a.id, a.topic, - * c.id, c.topic - * from USERS u - * inner join PHONENUMBERS p ON u.id = p.user_id - * inner join ARTICLES a ON u.id = a.user_id - * left outer join COMMENTS c ON a.id = c.article_id */ public function testMixedQueryMultipleDeepMixedFetchJoin() { @@ -717,7 +697,7 @@ class ObjectHydratorTest extends HydrationTestCase $this->assertEquals(1, count($result[1]->boards)); } - + public function testChainedJoinWithEmptyCollections() { $rsm = new ResultSetMapping; @@ -773,10 +753,10 @@ class ObjectHydratorTest extends HydrationTestCase $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 - * + * * @group bubu */ /*public function testChainedJoinWithScalars() @@ -825,25 +805,25 @@ class ObjectHydratorTest extends HydrationTestCase $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $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('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]); From d1bfd57fd9df6d26d3d8f0525aa86aa4a8a73499 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Wed, 2 Nov 2011 22:08:24 -0200 Subject: [PATCH 44/77] Initial code optimization in Hydrators. --- .../Internal/Hydration/AbstractHydrator.php | 78 +++++--- .../ORM/Internal/Hydration/ArrayHydrator.php | 81 ++++++--- .../ORM/Internal/Hydration/ObjectHydrator.php | 19 +- .../ORM/Internal/Hydration/ScalarHydrator.php | 23 ++- .../Hydration/SimpleObjectHydrator.php | 168 +++++++++++------- .../Hydration/SingleScalarHydrator.php | 33 ++-- 6 files changed, 257 insertions(+), 145 deletions(-) diff --git a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php index 61a0f48fa..493b1461c 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,15 +161,15 @@ 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. @@ -173,7 +188,7 @@ 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(); @@ -207,6 +222,7 @@ abstract class AbstractHydrator if (isset($cache[$key]['isScalar'])) { $rowData['scalars'][$cache[$key]['fieldName']] = $value; + continue; } @@ -220,10 +236,11 @@ abstract class AbstractHydrator 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) { @@ -250,9 +267,10 @@ 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(); @@ -294,8 +312,15 @@ abstract class AbstractHydrator 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 + */ + protected function registerManaged(ClassMetadata $class, $entity, array $data) { if ($class->isIdentifierComposite) { $id = array(); @@ -313,6 +338,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 cbc77d2d2..e25df2fea 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php @@ -25,8 +25,9 @@ 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 */ class ArrayHydrator extends AbstractHydrator { @@ -38,45 +39,54 @@ 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 $row, 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($row, $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; } @@ -111,7 +121,7 @@ class ArrayHydrator extends AbstractHydrator } $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)) { @@ -230,28 +240,45 @@ class ArrayHydrator extends AbstractHydrator { if ($coll === null) { unset($this->_resultPointers[$dqlAlias]); // Ticket #1228 + return; } + if ($index !== false) { $this->_resultPointers[$dqlAlias] =& $coll[$index]; + + return; + } + + if ( ! $coll) { return; - } else { - if ($coll) { - if ($oneToOne) { - $this->_resultPointers[$dqlAlias] =& $coll; - } else { - end($coll); - $this->_resultPointers[$dqlAlias] =& $coll[key($coll)]; - } - } } + + 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/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 3f787694b..d7d0281c4 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -29,8 +29,10 @@ 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. */ class ObjectHydrator extends AbstractHydrator @@ -53,7 +55,7 @@ class ObjectHydrator extends AbstractHydrator /** @override */ - protected function _prepare() + protected function prepare() { $this->_identifierMap = $this->_resultPointers = @@ -113,11 +115,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 +134,13 @@ class ObjectHydrator extends AbstractHydrator /** * {@inheritdoc} */ - protected function _hydrateAll() + 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); } // Take snapshots from all newly initialized collections @@ -278,13 +281,13 @@ class ObjectHydrator extends AbstractHydrator * @param array $cache The cache to use. * @param array $result The result array to fill. */ - protected function _hydrateRow(array $row, 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($row, $cache, $id, $nonemptyComponents); + $rowData = $this->gatherRowData($row, $cache, $id, $nonemptyComponents); // Extract scalar values. They're appended at the end. if (isset($rowData['scalars'])) { 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 e9f6395f9..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,82 +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] === "") { + + 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; } } @@ -133,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); } From 058242fa27b7f41aa0effc02072a94ff25496d38 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Thu, 3 Nov 2011 02:37:54 -0200 Subject: [PATCH 45/77] Fixed missing changes. --- .../ORM/Hydration/CustomHydratorTest.php | 2 +- .../ORM/Hydration/ObjectHydratorTest.php | 237 +++++++++++------- 2 files changed, 149 insertions(+), 90 deletions(-) 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 57c297cf0..268118d72 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php @@ -16,9 +16,10 @@ require_once __DIR__ . '/../../TestInit.php'; class ObjectHydratorTest extends HydrationTestCase { /** - * SELECT PARTIAL u.{id,name} FROM \Doctrine\Tests\Models\CMS\CmsUser u + * SELECT PARTIAL u.{id,name} + * FROM Doctrine\Tests\Models\CMS\CmsUser u */ - public function testSimpleEntityQuery() + public function testSimpleEntityScalarFieldsQuery() { $rsm = new ResultSetMapping; $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); @@ -30,22 +31,22 @@ 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); @@ -53,8 +54,10 @@ class ObjectHydratorTest extends HydrationTestCase } /** + * SELECT PARTIAL u.{id,name} + * FROM Doctrine\Tests\Models\CMS\CmsUser u + * * @group DDC-644 - * SELECT PARTIAL u.{id,name} FROM \Doctrine\Tests\Models\CMS\CmsUser u */ public function testSkipUnknownColumns() { @@ -69,20 +72,60 @@ 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 PARTIAL u.{id,name}, PARTIAL a.{id,topic} - * FROM \Doctrine\Tests\Models\CMS\CmsUser u, \Doctrine\Tests\Models\CMS\CmsArticle a + * SELECT u.id, + * u.name + * FROM Doctrine\Tests\Models\CMS\CmsUser u + */ + public function testScalarQueryWithoutResultVariables() + { + $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $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 */ public function testSimpleMultipleRootEntityQuery() { @@ -101,20 +144,18 @@ 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)); @@ -134,7 +175,8 @@ class ObjectHydratorTest extends HydrationTestCase } /** - * SELECT p FROM \Doctrine\Tests\Models\ECommerce\ECommerceProduct p + * SELECT p + * FROM Doctrine\Tests\Models\ECommerce\ECommerceProduct p */ public function testCreatesProxyForLazyLoadingWithForeignKeys() { @@ -150,8 +192,8 @@ class ObjectHydratorTest extends HydrationTestCase 'p__id' => '1', 'p__name' => 'Doctrine Book', 'p__shipping_id' => 42 - ) - ); + ) + ); $proxyInstance = new \Doctrine\Tests\Models\ECommerce\ECommerceShipping(); @@ -159,8 +201,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); @@ -169,32 +210,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, p, upper(u.name) nameUpper from User u JOIN u.phonenumbers p + * SELECT PARTIAL u.{id, status}, + * PARTIAL p.{phonenumber}, + * UPPER(u.name) nameUpper + * FROM User u + * JOIN u.phonenumbers p */ public function testMixedQueryFetchJoin() { $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->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( @@ -202,32 +247,32 @@ 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->assertInternalType('array', $result); + $this->assertInternalType('array', $result[0]); + $this->assertInternalType('array', $result[1]); $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[0][0]->phonenumbers); @@ -239,6 +284,7 @@ class ObjectHydratorTest extends HydrationTestCase // first user => 2 phonenumbers $this->assertEquals(2, count($result[0][0]->phonenumbers)); $this->assertEquals('ROMANB', $result[0]['nameUpper']); + // second user => 1 phonenumber $this->assertEquals(1, count($result[1][0]->phonenumbers)); $this->assertEquals('JWAGE', $result[1]['nameUpper']); @@ -249,8 +295,11 @@ class ObjectHydratorTest extends HydrationTestCase } /** - * select u, count(p.phonenumber) numPhones from User u - * join u.phonenumbers p group by u.id + * SELECT PARTIAL u.{id, status}, + * COUNT(p.phonenumber) numPhones + * FROM User u + * JOIN u.phonenumbers p + * GROUP BY u.id */ public function testMixedQueryNormalJoin() { @@ -267,25 +316,27 @@ 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']); + // second user => 1 phonenumber $this->assertEquals(1, $result[1]['numPhones']); $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); @@ -293,18 +344,23 @@ class ObjectHydratorTest extends HydrationTestCase } /** - * select u, p, upper(u.name) nameUpper from User u index by u.id - * join u.phonenumbers p indexby p.phonenumber + * SELECT u, + * p, + * UPPER(u.name) nameUpper + * FROM User u + * INDEX BY u.id + * JOIN u.phonenumbers p + * INDEX BY p.phonenumber */ public function testMixedQueryFetchJoinCustomIndex() { $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->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); @@ -321,31 +377,31 @@ 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[1])); - $this->assertTrue(is_array($result[2])); + + $this->assertInternalType('array', $result); + $this->assertInternalType('array', $result[1]); + $this->assertInternalType('array', $result[2]); // test the scalar values $this->assertEquals('ROMANB', $result[1]['nameUpper']); @@ -354,10 +410,13 @@ class ObjectHydratorTest extends HydrationTestCase $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][0]); $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[2][0]); $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][0]->phonenumbers); + // first user => 2 phonenumbers. notice the custom indexing by user id $this->assertEquals(2, count($result[1][0]->phonenumbers)); + // second user => 1 phonenumber. notice the custom indexing by user id $this->assertEquals(1, count($result[2][0]->phonenumbers)); + // test the custom indexing of the phonenumbers $this->assertTrue(isset($result[1][0]->phonenumbers['42'])); $this->assertTrue(isset($result[1][0]->phonenumbers['43'])); @@ -1130,15 +1189,15 @@ 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); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); @@ -1169,13 +1228,13 @@ class ObjectHydratorTest extends HydrationTestCase '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); @@ -1198,21 +1257,21 @@ class ObjectHydratorTest extends HydrationTestCase $rsm->addScalarResult('sclr0', 'nameUpper'); $rsm->addIndexByScalar('sclr0'); - // Faked result set + // Faked result set $resultSet = array( //row1 array( 'sclr0' => 'ROMANB', - ), + ), array( '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(array('ROMANB' => array('nameUpper' => 'ROMANB'), 'JWAGE' => array('nameUpper' => 'JWAGE')), $result); } From 3c31d888104107f5759bcef36a8db774324f3daf Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Thu, 3 Nov 2011 02:44:50 -0200 Subject: [PATCH 46/77] Major optimizations in SqlWalker code, reducing overhead, reducing lookahead checks. --- lib/Doctrine/ORM/Query/QueryException.php | 5 + lib/Doctrine/ORM/Query/SqlWalker.php | 550 +++++++----------- .../ORM/Query/SelectSqlGenerationTest.php | 8 +- 3 files changed, 235 insertions(+), 328 deletions(-) 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/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 13021a4eb..b0f919549 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -252,34 +252,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; + + $sqlParts = array(); foreach ($class->getQuotedIdentifierColumnNames($this->_platform) as $columnName) { - if ($first) $first = false; else $sql .= ' AND '; - - $sql .= $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $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; + // Ignore subclassing inclusion if partial objects is disallowed + if ($this->_query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) { + return $sql; + } + + // LEFT JOIN child class tables + foreach ($class->subClasses as $subClassName) { + $subClass = $this->_em->getClassMetadata($subClassName); + $tableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); - foreach ($class->getQuotedIdentifierColumnNames($this->_platform) as $columnName) { - if ($first) $first = false; else $sql .= ' AND '; + $sql .= ' LEFT JOIN ' . $subClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON '; - $sql .= $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; - } + $sqlParts = array(); + + foreach ($subClass->getQuotedIdentifierColumnNames($this->_platform) as $columnName) { + $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; } + + $sql .= implode(' AND ', $sqlParts); } return $sql; @@ -287,28 +293,24 @@ class SqlWalker implements TreeWalker private function _generateOrderedCollectionOrderByItems() { - $sql = ''; - + $sqlParts = array(); + 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(); - - if ($sql != '') { - $sql .= ', '; - } - - $sql .= $this->getSQLTableAlias($tableName, $dqlAlias) . '.' - . $qComp['metadata']->getQuotedColumnName($fieldName, $this->_platform) . ' ' . $orientation; - } + 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($class->name)->getOwningTable($fieldName) + : $qComp['metadata']->getTableName(); + + $sqlParts[] = $this->getSQLTableAlias($tableName, $dqlAlias) . '.' . $columnName . ' ' . $orientation; } } - return $sql; + return implode(', ', $sqlParts); } /** @@ -319,36 +321,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->isInheritanceTypeSingleTable()) continue; + + $conn = $this->_em->getConnection(); + $values = array(); - 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); - } - - 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; } /** @@ -375,16 +372,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($class->name); + 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 $class) { + if ( ! $class->isVersioned) { + throw \Doctrine\ORM\OptimisticLockException::lockFailed($class->name); + } } - } + break; + + default: + throw \Doctrine\ORM\Query\QueryException::invalidLockMode(); } } @@ -529,8 +535,8 @@ 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']); @@ -538,44 +544,44 @@ class SqlWalker implements TreeWalker $this->_rsm->setDiscriminatorColumn($dqlAlias, $columnAlias); $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $discrColumn['fieldName']); + } + + // 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; - // 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); - } + $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); + foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { + $columnAlias = $this->getSQLColumnAlias($srcColumn); - $sqlSelectExpressions[] = $sqlTableAlias . '.' . $srcColumn . ' AS ' . $columnAlias; + $sqlSelectExpressions[] = $sqlTableAlias . '.' . $srcColumn . ' AS ' . $columnAlias; - $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $srcColumn, (isset($assoc['id']) && $assoc['id'] === true)); - } - } - } + $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); + } + + // Add foreign key columns of subclasses + foreach ($class->subClasses as $subClassName) { + $subClass = $this->_em->getClassMetadata($subClassName); + $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); - foreach ($class->associationMappings as $assoc) { - if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { - foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { - $columnAlias = $this->getSQLColumnAlias($srcColumn); + 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; - $sqlSelectExpressions[] = $sqlTableAlias . '.' . $srcColumn . ' AS ' . $columnAlias; - - $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $srcColumn, (isset($assoc['id']) && $assoc['id'] === true)); - } - } + $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $srcColumn); } } } @@ -654,15 +660,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); } /** @@ -673,16 +677,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); } @@ -706,22 +704,11 @@ class SqlWalker implements TreeWalker */ public function walkJoinVariableDeclaration($joinVarDecl) { - $join = $joinVarDecl->join; + $join = $joinVarDecl->join; $joinType = $join->joinType; - - if ($joinVarDecl->indexBy) { - // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. - $this->_rsm->addIndexBy( - $joinVarDecl->indexBy->simpleStateFieldPathExpression->identificationVariable, - $joinVarDecl->indexBy->simpleStateFieldPathExpression->field - ); - } - - if ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) { - $sql = ' LEFT JOIN '; - } else { - $sql = ' INNER JOIN '; - } + $sql = ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) + ? ' LEFT JOIN ' + : ' INNER JOIN '; $joinAssocPathExpr = $join->joinAssociationPathExpression; $joinedDqlAlias = $join->aliasIdentificationVariable; @@ -737,10 +724,8 @@ class SqlWalker implements TreeWalker // 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) { @@ -757,7 +742,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; @@ -984,199 +968,133 @@ 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']; - - $resultAlias = ( ! $selectExpression->fieldIdentificationVariable) - ? $fieldName - : $selectExpression->fieldIdentificationVariable; - - $tableName = ($class->isInheritanceTypeJoined()) - ? $this->_em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName) - : $class->getTableName(); - - $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); - $columnName = $class->getQuotedColumnName($fieldName, $this->_platform); - - $columnAlias = $this->getSQLColumnAlias($columnName); - $sql .= $sqlTableAlias . '.' . $columnName . ' AS ' . $columnAlias; - - if ( ! $hidden) { - $this->_rsm->addScalarResult($columnAlias, $resultAlias); - $this->_scalarFields[$dqlAlias][$fieldName] = $columnAlias; - } - } else if ($expr instanceof AST\AggregateExpression) { - if ( ! $selectExpression->fieldIdentificationVariable) { - $resultAlias = $this->_scalarResultCounter++; - } else { - $resultAlias = $selectExpression->fieldIdentificationVariable; - } - - $columnAlias = $this->getSQLColumnAlias('sclr'); - $sql .= $this->walkAggregateExpression($expr) . ' AS ' . $columnAlias; - $this->_scalarResultAliasMap[$resultAlias] = $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 = $this->getSQLColumnAlias('sclr'); - $sql .= '(' . $this->walkSubselect($expr) . ') AS '.$columnAlias; - $this->_scalarResultAliasMap[$resultAlias] = $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 = $this->getSQLColumnAlias('sclr'); - $sql .= $this->walkFunction($expr) . ' AS ' . $columnAlias; - $this->_scalarResultAliasMap[$resultAlias] = $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 = $this->getSQLColumnAlias('sclr'); - if ($expr instanceof AST\Literal) { - $sql .= $this->walkLiteral($expr) . ' AS ' .$columnAlias; - } else { - $sql .= $this->walkSimpleArithmeticExpression($expr) . ' AS ' . $columnAlias; - } - - $this->_scalarResultAliasMap[$resultAlias] = $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 = $this->getSQLColumnAlias('sclr'); - $sql .= $this->walkCaseExpression($expr) . ' AS ' . $columnAlias; - - $this->_scalarResultAliasMap[$resultAlias] = $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); + + $sql .= $sqlTableAlias . '.' . $columnName . ' AS ' . $columnAlias; - $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); - } + if ( ! $hidden) { + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + $this->_scalarFields[$dqlAlias][$fieldName] = $columnAlias; + } + break; + + 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++; + + $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; + + $this->_scalarResultAliasMap[$resultAlias] = $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); + 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; - foreach ($subClass->fieldMappings as $fieldName => $mapping) { - if (isset($mapping['inherited']) || $partialFieldSet && !in_array($fieldName, $partialFieldSet)) { - continue; - } + 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(); + } - if ($beginning) $beginning = false; else $sql .= ', '; + $queryComp = $this->_queryComponents[$dqlAlias]; + $class = $queryComp['metadata']; - $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); - $sql .= $sqlTableAlias . '.' . $subClass->getQuotedColumnName($fieldName, $this->_platform) - . ' AS ' . $columnAlias; + if ( ! isset($this->_selectedClasses[$dqlAlias])) { + $this->_selectedClasses[$dqlAlias] = $class; + } - $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); + $beginning = true; + + // 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 .= ', '; + $tableName = (isset($mapping['inherited'])) + ? $this->_em->getClassMetadata($mapping['inherited'])->getTableName() + : $class->getTableName(); - $columnAlias = $this->getSQLColumnAlias($srcColumn); - $sql .= $sqlTableAlias . '.' . $srcColumn . ' AS ' . $columnAlias; + if ($beginning) $beginning = false; else $sql .= ', '; - $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $srcColumn); + $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); + $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); + $sql .= $sqlTableAlias . '.' . $class->getQuotedColumnName($fieldName, $this->_platform) + . ' 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; } + + if ($beginning) $beginning = false; else $sql .= ', '; + + $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); + $sql .= $sqlTableAlias . '.' . $subClass->getQuotedColumnName($fieldName, $this->_platform) + . ' AS ' . $columnAlias; + + $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); } } } - } } return $sql; @@ -1302,27 +1220,11 @@ class SqlWalker implements TreeWalker break; case ($expr instanceof AST\Functions\FunctionNode): - $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->_scalarResultCounter++; - - $columnAlias = $this->getSQLColumnAlias('sclr'); - $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 = $this->getSQLColumnAlias('sclr'); - $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): @@ -1332,7 +1234,7 @@ class SqlWalker implements TreeWalker $columnAlias = $this->getSQLColumnAlias('sclr'); $this->_scalarResultAliasMap[$alias] = $columnAlias; - $sql .= $this->walkCaseExpression($expr) . ' AS ' . $columnAlias; + $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; break; default: // IdentificationVariable diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 499fb8c4c..8eaaf37bb 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -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() ); } @@ -1120,7 +1120,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $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 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) ); } @@ -1144,7 +1144,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $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 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) ); } @@ -1264,7 +1264,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $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 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) ); } From 305da5b8ff0ddfc667c1a770404a22f97f3270b1 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Thu, 3 Nov 2011 02:49:50 -0200 Subject: [PATCH 47/77] Added missing indexBy. --- lib/Doctrine/ORM/Query/SqlWalker.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index b0f919549..03933d3d9 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -709,6 +709,14 @@ class SqlWalker implements TreeWalker $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. + $this->_rsm->addIndexBy( + $joinVarDecl->indexBy->simpleStateFieldPathExpression->identificationVariable, + $joinVarDecl->indexBy->simpleStateFieldPathExpression->field + ); + } $joinAssocPathExpr = $join->joinAssociationPathExpression; $joinedDqlAlias = $join->aliasIdentificationVariable; From ca438fa1109142ef301bf15d72e8dd5af76d4d04 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 3 Nov 2011 09:20:41 +0100 Subject: [PATCH 48/77] Eagerly fetched entities should only be inner joined if they are loaded from the owning side. --- .../ORM/Persisters/BasicEntityPersister.php | 2 +- .../Functional/OneToOneEagerLoadingTest.php | 93 ++++++++++++++++--- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 9d04cf5f1..8663f3014 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -1019,7 +1019,7 @@ class BasicEntityPersister $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']); $owningAssoc = $eagerEntity->getAssociationMapping($assoc['mappedBy']); - $this->_selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($owningAssoc['joinColumns']); + $this->_selectJoinSql .= ' LEFT JOIN'; $this->_selectJoinSql .= ' ' . $eagerEntity->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) . ' ON '; diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php index 5361aad45..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); @@ -116,21 +116,31 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertNotNull($waggon->train); } - public function testEagerLoadWithNullableColumnsGeneratesLeftJoin() + public function testEagerLoadWithNullableColumnsGeneratesLeftJoinOnBothSides() { - $train = new Train(); + $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 FROM Train t0 LEFT JOIN TrainDriver t3 ON t0.driver_id = t3.id WHERE t0.id = ?", + "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 testEagerLoadWithNonNullableColumnsGeneratesInnerJoin() + public function testEagerLoadWithNonNullableColumnsGeneratesInnerJoinOnOwningSide() { $waggon = new Waggon(); $this->_em->persist($waggon); @@ -139,7 +149,22 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $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 FROM Waggon t0 INNER JOIN Train t3 ON t0.train_id = t3.id WHERE t0.id = ?", + "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'] ); } @@ -161,14 +186,20 @@ class Train * @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) @@ -177,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); @@ -210,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 */ From 9c4c06c42219866b2bb3d99785b2abb2bb7f4952 Mon Sep 17 00:00:00 2001 From: everzet Date: Thu, 3 Nov 2011 16:24:47 +0200 Subject: [PATCH 49/77] optimized PreFlush (moved into computeChangeSet function) --- lib/Doctrine/ORM/UnitOfWork.php | 35 +++++---------------------------- 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index d8b640c47..ea7138c98 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -259,36 +259,6 @@ class UnitOfWork implements PropertyChangedListener */ public function commit($entity = null) { - // Run preFlush lifecycle callback for new entities - foreach ($this->entityInsertions as $classEntity) { - $class = $this->em->getClassMetadata(get_class($classEntity)); - - // Skip class if instances are read-only - if ($class->isReadOnly) { - continue; - } - - if (isset($class->lifecycleCallbacks[Events::preFlush])) { - $class->invokeLifecycleCallbacks(Events::preFlush, $classEntity); - } - } - - // Run preFlush lifecycle callback for persisted entities - foreach ($this->identityMap as $className => $classEntities) { - $class = $this->em->getClassMetadata($className); - - // Skip class if instances are read-only - if ($class->isReadOnly) { - continue; - } - - if (isset($class->lifecycleCallbacks[Events::preFlush])) { - foreach ($classEntities as $classEntity) { - $class->invokeLifecycleCallbacks(Events::preFlush, $classEntity); - } - } - } - // Raise preFlush if ($this->evm->hasListeners(Events::preFlush)) { $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->em)); @@ -516,6 +486,11 @@ class UnitOfWork implements PropertyChangedListener return; } + // 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); From c6a3ff4da54e904311e87a2fcaa37906aed61582 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Sat, 5 Nov 2011 03:09:14 -0200 Subject: [PATCH 50/77] Optimizations in UnitOfWork. --- .../ORM/Mapping/ClassMetadataInfo.php | 12 +- lib/Doctrine/ORM/UnitOfWork.php | 261 ++++++++++-------- 2 files changed, 154 insertions(+), 119 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 10f7bc4b6..592367ab7 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. */ @@ -1504,14 +1504,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; } diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 6add3bd6b..bfe18fc7f 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -290,8 +290,8 @@ class UnitOfWork implements PropertyChangedListener $commitOrder = $this->getCommitOrder(); $conn = $this->em->getConnection(); - $conn->beginTransaction(); + try { if ($this->entityInsertions) { foreach ($commitOrder as $class) { @@ -312,13 +312,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 +330,7 @@ class UnitOfWork implements PropertyChangedListener } catch (Exception $e) { $this->em->close(); $conn->rollback(); + throw $e; } @@ -397,7 +396,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 +407,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 +420,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,10 +434,8 @@ class UnitOfWork implements PropertyChangedListener public function getEntityChangeSet($entity) { $oid = spl_object_hash($entity); - if (isset($this->entityChangeSets[$oid])) { - return $this->entityChangeSets[$oid]; - } - return array(); + + return isset($this->entityChangeSets[$oid]) ? $this->entityChangeSets[$oid] : array(); } /** @@ -486,11 +485,7 @@ class UnitOfWork implements PropertyChangedListener 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); @@ -499,17 +494,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; } } @@ -519,13 +517,17 @@ 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); } } @@ -533,54 +535,65 @@ class UnitOfWork implements PropertyChangedListener } 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; + } + + 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. - } + $assoc = $class->associationMappings[$propName]; + + 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); } } @@ -601,24 +614,21 @@ class UnitOfWork implements PropertyChangedListener $class = $this->em->getClassMetadata($className); // Skip class if instances are read-only - if ($class->isReadOnly) { - continue; - } + if ($class->isReadOnly) continue; // If change tracking is explicit or happens through notification, then only compute // changes on entities of that type that are explicitly marked for synchronization. - $entitiesToProcess = ! $class->isChangeTrackingDeferredImplicit() ? - (isset($this->scheduledForDirtyCheck[$className]) ? - $this->scheduledForDirtyCheck[$className] : array()) - : $entities; + $entitiesToProcess = ! $class->isChangeTrackingDeferredImplicit() + ? (isset($this->scheduledForDirtyCheck[$className]) ? $this->scheduledForDirtyCheck[$className] : array()) + : $entities; foreach ($entitiesToProcess as $entity) { // Ignore uninitialized proxy objects - if ($entity instanceof Proxy && ! $entity->__isInitialized__) { - continue; - } + 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); } @@ -634,75 +644,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) { + // 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); @@ -730,16 +761,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); @@ -751,6 +783,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)) { From ea69d9ca0cc20e27a14770414efc25414dc7ab0f Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Sat, 5 Nov 2011 19:21:35 -0200 Subject: [PATCH 51/77] Fixed wrong optimization. Optimized more pieces of code in UnitOfWork. --- lib/Doctrine/ORM/UnitOfWork.php | 133 ++++++++++++++++++-------------- 1 file changed, 77 insertions(+), 56 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index bfe18fc7f..d73d625c6 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -435,7 +435,11 @@ class UnitOfWork implements PropertyChangedListener { $oid = spl_object_hash($entity); - return isset($this->entityChangeSets[$oid]) ? $this->entityChangeSets[$oid] : array(); + if (isset($this->entityChangeSets[$oid])) { + return $this->entityChangeSets[$oid]; + } + + return array(); } /** @@ -531,6 +535,7 @@ class UnitOfWork implements PropertyChangedListener $changeSet[$propName] = array(null, $actualValue); } } + $this->entityChangeSets[$oid] = $changeSet; } else { // Entity is "fully" MANAGED: it was already fully persisted before @@ -1195,61 +1200,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]; } /** @@ -1266,13 +1277,17 @@ class UnitOfWork implements PropertyChangedListener $oid = spl_object_hash($entity); $classMetadata = $this->em->getClassMetadata(get_class($entity)); $idHash = implode(' ', $this->entityIdentifiers[$oid]); + if ($idHash === '') { 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; } @@ -1305,8 +1320,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; } /** @@ -1318,11 +1336,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; } From 96aa25fb3e5e3066ff89d569e75a84b7750774d1 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Sun, 6 Nov 2011 02:03:34 -0200 Subject: [PATCH 52/77] Optimized more pieces of code in UnitOfWork. --- lib/Doctrine/ORM/UnitOfWork.php | 446 ++++++++++++++++++++------------ 1 file changed, 283 insertions(+), 163 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index d73d625c6..01473f875 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -623,9 +623,19 @@ 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 @@ -800,6 +810,7 @@ class UnitOfWork implements PropertyChangedListener if (isset($this->entityChangeSets[$oid])) { $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet); } + $this->originalEntityData[$oid] = $actualData; } } @@ -813,20 +824,20 @@ 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; } } @@ -835,24 +846,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)); } } } @@ -868,35 +881,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)); } } } @@ -915,27 +934,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)); } } } @@ -948,51 +972,57 @@ 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); } } } @@ -1013,9 +1043,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."); } @@ -1046,9 +1078,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."); } @@ -1071,13 +1105,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; } /** @@ -1093,9 +1130,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)]); } @@ -1113,22 +1158,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; } } @@ -1153,9 +1201,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]); } /** @@ -1172,18 +1221,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; } @@ -1274,18 +1329,19 @@ 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; @@ -1373,6 +1429,7 @@ class UnitOfWork implements PropertyChangedListener public function persist($entity) { $visited = array(); + $this->doPersist($entity, $visited); } @@ -1388,6 +1445,7 @@ class UnitOfWork implements PropertyChangedListener private function doPersist($entity, array &$visited) { $oid = spl_object_hash($entity); + if (isset($visited[$oid])) { return; // Prevent infinite recursion } @@ -1409,19 +1467,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); @@ -1435,6 +1498,7 @@ class UnitOfWork implements PropertyChangedListener public function remove($entity) { $visited = array(); + $this->doRemove($entity, $visited); } @@ -1451,6 +1515,7 @@ class UnitOfWork implements PropertyChangedListener private function doRemove($entity, array &$visited) { $oid = spl_object_hash($entity); + if (isset($visited[$oid])) { return; // Prevent infinite recursion } @@ -1461,26 +1526,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)); } } @@ -1498,6 +1569,7 @@ class UnitOfWork implements PropertyChangedListener public function merge($entity) { $visited = array(); + return $this->doMerge($entity, $visited); } @@ -1514,6 +1586,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 } @@ -1526,9 +1599,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(); } @@ -1539,9 +1612,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) { @@ -1556,19 +1631,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); @@ -1631,6 +1708,7 @@ class UnitOfWork implements PropertyChangedListener 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); } @@ -1638,11 +1716,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); } @@ -1651,10 +1731,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); } @@ -2031,7 +2113,7 @@ class UnitOfWork implements PropertyChangedListener public function isCollectionScheduledForDeletion(PersistentCollection $coll) { - return isset( $this->collectionsDeletions[spl_object_hash($coll)] ); + return isset($this->collectionsDeletions[spl_object_hash($coll)]); } /** @@ -2235,6 +2317,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)); } @@ -2247,17 +2330,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) { $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))) + ); } } @@ -2269,15 +2355,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; } } @@ -2302,9 +2389,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(); } @@ -2373,6 +2462,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; } @@ -2394,34 +2484,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]; } @@ -2429,19 +2530,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]; } @@ -2456,9 +2567,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); } @@ -2485,7 +2598,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]); @@ -2496,6 +2609,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); } @@ -2561,7 +2675,11 @@ class UnitOfWork implements PropertyChangedListener { if ($obj instanceof Proxy) { $obj->__load(); - } else if ($obj instanceof PersistentCollection) { + + return; + } + + if ($obj instanceof PersistentCollection) { $obj->initialize(); } } @@ -2592,6 +2710,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; } @@ -2607,6 +2726,7 @@ class UnitOfWork implements PropertyChangedListener if ( ! is_object($object) ) { throw new InvalidArgumentException("Managed entity required"); } + return isset($this->readOnlyObjects[spl_object_hash($object)]); } } From 32b8d77580294af45b59166fcda171d79ba31f18 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Mon, 7 Nov 2011 01:27:20 -0200 Subject: [PATCH 53/77] Fixed CS issues. More tiny optimizations in UnitOfWork. --- lib/Doctrine/ORM/UnitOfWork.php | 63 ++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 01473f875..fab0df4a1 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -366,6 +366,7 @@ class UnitOfWork implements PropertyChangedListener { foreach ($this->entityInsertions as $entity) { $class = $this->em->getClassMetadata(get_class($entity)); + $this->computeChangeSet($class, $entity); } } @@ -548,27 +549,37 @@ class UnitOfWork implements PropertyChangedListener foreach ($actualData as $propName => $actualValue) { // skip field, its a partially omitted one! - if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) continue; + if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) { + continue; + } $orgValue = $originalData[$propName]; // skip if value havent changed - if ($orgValue === $actualValue) continue; + if ($orgValue === $actualValue) { + continue; + } // if regular field if ( ! isset($class->associationMappings[$propName])) { - if ($isChangeTrackingNotify) continue; + 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; + if (isset($this->collectionDeletions[$coid])) { + continue; + } $this->collectionDeletions[$coid] = $orgValue; $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored. @@ -576,8 +587,6 @@ class UnitOfWork implements PropertyChangedListener continue; } - $assoc = $class->associationMappings[$propName]; - if ($assoc['type'] & ClassMetadata::TO_ONE) { if ($assoc['isOwningSide']) { $changeSet[$propName] = array($orgValue, $actualValue); @@ -619,7 +628,9 @@ class UnitOfWork implements PropertyChangedListener $class = $this->em->getClassMetadata($className); // Skip class if instances are read-only - if ($class->isReadOnly) continue; + if ($class->isReadOnly) { + continue; + } // If change tracking is explicit or happens through notification, then only compute // changes on entities of that type that are explicitly marked for synchronization. @@ -639,7 +650,9 @@ class UnitOfWork implements PropertyChangedListener foreach ($entitiesToProcess as $entity) { // Ignore uninitialized proxy objects - if ($entity instanceof Proxy && ! $entity->__isInitialized__) continue; + if ($entity instanceof Proxy && ! $entity->__isInitialized__) { + continue; + } // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here. $oid = spl_object_hash($entity); @@ -673,8 +686,8 @@ class UnitOfWork implements PropertyChangedListener $this->visitedCollections[$coid] = $value; } - // Look through the entities, and in any of their associations, for transient (new) - // entities, recursively. ("Persistence by reachability") + // 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']); @@ -830,7 +843,9 @@ class UnitOfWork implements PropertyChangedListener $hasListeners = $this->evm->hasListeners(Events::postPersist); foreach ($this->entityInsertions as $oid => $entity) { - if (get_class($entity) !== $className) continue; + if (get_class($entity) !== $className) { + continue; + } $persister->addInsert($entity); @@ -987,7 +1002,9 @@ class UnitOfWork implements PropertyChangedListener foreach ($entityChangeSet as $oid => $entity) { $className = get_class($entity); - if ($calc->hasClass($className)) continue; + if ($calc->hasClass($className)) { + continue; + } $class = $this->em->getClassMetadata($className); $calc->addClass($class); @@ -998,7 +1015,9 @@ class UnitOfWork implements PropertyChangedListener // Calculate dependencies for new nodes while ($class = array_pop($newNodes)) { foreach ($class->associationMappings as $assoc) { - if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) continue; + if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) { + continue; + } $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); @@ -1011,7 +1030,9 @@ class UnitOfWork implements PropertyChangedListener $calc->addDependency($targetClass, $class); // If the target class has mapped subclasses, these share the same dependency. - if ( ! $targetClass->subClasses) continue; + if ( ! $targetClass->subClasses) { + continue; + } foreach ($targetClass->subClasses as $subClassName) { $targetSubClass = $this->em->getClassMetadata($subClassName); @@ -1704,6 +1725,7 @@ 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(); @@ -1968,6 +1990,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; @@ -1978,6 +2001,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) { @@ -2065,6 +2089,7 @@ class UnitOfWork implements PropertyChangedListener $this->collectionUpdates = $this->extraUpdates = $this->orphanRemovals = array(); + if ($this->commitOrderCalculator !== null) { $this->commitOrderCalculator->clear(); } @@ -2106,9 +2131,15 @@ 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) @@ -2338,7 +2369,7 @@ class UnitOfWork implements PropertyChangedListener $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( From 6707129a3ee557b24c957e86d9d5f77e1b9c72e9 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 8 Nov 2011 09:43:06 +0100 Subject: [PATCH 54/77] Added type casts to 'non-lazy' identifiers in proxies. --- lib/Doctrine/ORM/Proxy/ProxyFactory.php | 7 +++++- .../ORM/Functional/ReferenceProxyTest.php | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index 490c3a119..2a6cd1af9 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -210,8 +210,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', 'bigint', '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"; @@ -237,6 +241,7 @@ class ProxyFactory 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/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php index b89f3d04e..96427600b 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'; @@ -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->assertTrue($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(); From 0cc176aae202b7142a09ca2d49ceb74c7cedb32b Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 8 Nov 2011 09:47:33 +0100 Subject: [PATCH 55/77] Do not cast BigInt to (int) --- lib/Doctrine/ORM/Proxy/ProxyFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index 2a6cd1af9..95b935b91 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -212,7 +212,7 @@ class ProxyFactory if ($this->isShortIdentifierGetter($method, $class)) { $identifier = lcfirst(substr($method->getName(), 3)); - $cast = in_array($class->fieldMappings[$identifier]['type'], array('integer', 'bigint', 'smallint')) ? '(int) ' : ''; + $cast = in_array($class->fieldMappings[$identifier]['type'], array('integer', 'smallint')) ? '(int) ' : ''; $methods .= ' if ($this->__isInitialized__ === false) {' . "\n"; $methods .= ' return ' . $cast . '$this->_identifier["' . $identifier . '"];' . "\n"; From e99b8004065fe80c27af20c15ebad923619a5c09 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 8 Nov 2011 10:01:22 +0100 Subject: [PATCH 56/77] Throw exception on invalid fetch mode in annotations --- .../ORM/Mapping/Driver/AnnotationDriver.php | 24 ++++++++++++++--- lib/Doctrine/ORM/Mapping/MappingException.php | 7 ++++- .../ORM/Mapping/AnnotationDriverTest.php | 27 ++++++++++++++++++- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 36e3dcb01..d886b5d04 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; @@ -536,6 +536,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/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/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; +} From 39ed719c4cda4466f234b80543f3b2ec0b83b31e Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 8 Nov 2011 13:15:09 +0100 Subject: [PATCH 57/77] Remove invalid(?) exception for now --- lib/Doctrine/ORM/Query/ResultSetMapping.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Query/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index fcfc9b834..5ac99ca82 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMapping.php +++ b/lib/Doctrine/ORM/Query/ResultSetMapping.php @@ -165,10 +165,13 @@ class ResultSetMapping 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) { throw new \LogicException("Cannot add index by for dql alias " . $alias . " and field " . $fieldName . " without calling addFieldResult() for them before."); } + */ } /** @@ -435,4 +438,4 @@ class ResultSetMapping $this->isIdentifierColumn[$alias][$columnName] = true; } } -} \ No newline at end of file +} From c391287cc4909f38f6114403f466f2b3c83b0c48 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Tue, 8 Nov 2011 18:36:18 -0200 Subject: [PATCH 58/77] More optimizations and increased code readability in Id Generators. --- lib/Doctrine/ORM/Id/SequenceGenerator.php | 9 ++++++--- lib/Doctrine/ORM/Id/TableGenerator.php | 8 +++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/Doctrine/ORM/Id/SequenceGenerator.php b/lib/Doctrine/ORM/Id/SequenceGenerator.php index 0d564ed32..dd7de60ba 100644 --- a/lib/Doctrine/ORM/Id/SequenceGenerator.php +++ b/lib/Doctrine/ORM/Id/SequenceGenerator.php @@ -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); + $sql = $conn->getDatabasePlatform()->getSequenceNextValSQL($this->_sequenceName); + $this->_nextValue = $conn->fetchColumn($sql); - $this->_maxValue = $this->_nextValue + $this->_allocationSize; + $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 From 2ddfc6af5a81123b45c5e19fd4e1699366c0fc11 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 9 Nov 2011 08:17:46 +0100 Subject: [PATCH 59/77] Fixed failing tests in PHPUnit 3.6.2 (expecting \Exception was deprecated) --- .../OneToManyUnidirectionalAssociationTest.php | 13 ++++++++++--- .../Tests/ORM/Functional/Ticket/DDC117Test.php | 11 +++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) 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/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."); } /** From 3131103801bb77ecf9a42dd812fcd0630b2e3c88 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 9 Nov 2011 22:09:35 +0100 Subject: [PATCH 60/77] Failing test case --- .../ORM/Functional/Ticket/DDC1458Test.php | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1458Test.php 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..3b13a540c --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1458Test.php @@ -0,0 +1,133 @@ +_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; + + private $i = 0; + + 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; + } +} From 9c9f85ed4b78218844a45d52d2e37613fb1e77f5 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 9 Nov 2011 22:13:06 +0100 Subject: [PATCH 61/77] Only refresh the given entity if an entity is specified in the query hints --- lib/Doctrine/ORM/UnitOfWork.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index fab0df4a1..11280b597 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -2195,6 +2195,11 @@ class UnitOfWork implements PropertyChangedListener } } else { $overrideLocalValues = isset($hints[Query::HINT_REFRESH]); + + // If a 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) { From 1f55351f194df3317aa6648ceb568018c4ac94df Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 10 Nov 2011 16:16:55 +0100 Subject: [PATCH 62/77] Cleanup --- lib/Doctrine/ORM/UnitOfWork.php | 2 +- tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1458Test.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 11280b597..c3bbe0fe8 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -2196,7 +2196,7 @@ class UnitOfWork implements PropertyChangedListener } else { $overrideLocalValues = isset($hints[Query::HINT_REFRESH]); - // If a only a specific entity is set to refresh, check that it's the one + // 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; } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1458Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1458Test.php index 3b13a540c..2a0541afd 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1458Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1458Test.php @@ -74,8 +74,6 @@ class TestEntity */ protected $additional; - private $i = 0; - public function getValue() { return $this->value; From a14ba1e561e6e37c54593662d4f3f8f55bd1945f Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 12 Nov 2011 09:43:37 +0100 Subject: [PATCH 63/77] DDC-1237 - Remove dependency to mbstring --- lib/Doctrine/ORM/Query/Expr/Composite.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 From 450d92872aa55e484024a48c7bd474d98270b9f0 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 12 Nov 2011 12:56:44 +0100 Subject: [PATCH 64/77] Forward compatibility with DBAL master --- tests/Doctrine/Tests/Mocks/ConnectionMock.php | 22 +++++----- .../Tests/Mocks/DatabasePlatformMock.php | 11 ++++- .../Tests/Mocks/HydratorMockStatement.php | 40 ++++++++++++------- 3 files changed, 45 insertions(+), 28 deletions(-) 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 From 01697fee3db000b317e4ecb13dc9b0f0acf7abb0 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 12 Nov 2011 22:16:39 +0100 Subject: [PATCH 65/77] Fix failing PostgreSQL tests --- .../ORM/Functional/ReferenceProxyTest.php | 12 ++-- .../SchemaTool/PostgreSqlSchemaToolTest.php | 56 ++++++++++--------- .../ORM/Functional/Ticket/DDC1151Test.php | 12 ++-- 3 files changed, 42 insertions(+), 38 deletions(-) diff --git a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php index 96427600b..9cd216066 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php @@ -98,7 +98,7 @@ class ReferenceProxyTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertTrue($clone->isCloned); $this->assertFalse($entity->isCloned); } - + /** * @group DDC-733 */ @@ -108,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 */ @@ -124,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()); } @@ -180,7 +180,7 @@ class ReferenceProxyTest extends \Doctrine\Tests\OrmFunctionalTestCase $entity = $product->getShipping(); $this->assertFalse($entity->__isInitialized__, "Pre-Condition: Object is unitialized proxy."); $this->assertEquals($id, $entity->getId()); - $this->assertTrue($id === $entity->getId(), "Check that the id's are the same value, and type."); + $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."); } 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/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; } From 5aeabcb445acf0f3ae3809ca07094d44d86f59b6 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 13 Nov 2011 15:36:48 +0100 Subject: [PATCH 66/77] DDC-1490 - Fix id generation of sequence and identity to cast values to int --- lib/Doctrine/ORM/Id/IdentityGenerator.php | 2 +- lib/Doctrine/ORM/Id/SequenceGenerator.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) 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 dd7de60ba..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. * @@ -60,11 +60,11 @@ class SequenceGenerator extends AbstractIdGenerator implements Serializable // Allocate new values $conn = $em->getConnection(); $sql = $conn->getDatabasePlatform()->getSequenceNextValSQL($this->_sequenceName); - - $this->_nextValue = $conn->fetchColumn($sql); + + $this->_nextValue = (int)$conn->fetchColumn($sql); $this->_maxValue = $this->_nextValue + $this->_allocationSize; } - + return $this->_nextValue++; } @@ -99,7 +99,7 @@ class SequenceGenerator extends AbstractIdGenerator implements Serializable public function unserialize($serialized) { $array = unserialize($serialized); - + $this->_sequenceName = $array['sequenceName']; $this->_allocationSize = $array['allocationSize']; } From f7c46c7b3330dc5e56c5241de67b2abe66501548 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 13 Nov 2011 15:45:06 +0100 Subject: [PATCH 67/77] DDC-1491 - Fix Schema Validator bug --- lib/Doctrine/ORM/Tools/SchemaValidator.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/SchemaValidator.php b/lib/Doctrine/ORM/Tools/SchemaValidator.php index 2c0bd94ff..cb3c9e515 100644 --- a/lib/Doctrine/ORM/Tools/SchemaValidator.php +++ b/lib/Doctrine/ORM/Tools/SchemaValidator.php @@ -173,14 +173,14 @@ class SchemaValidator 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(), $assoc['relationToTargetKeyColumns'])) . + "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(), $assoc['relationToSourceKeyColumns'])) . + "however '" . implode(", ", array_diff($class->getIdentifierColumnNames(), array_values($assoc['relationToSourceKeyColumns']))) . "' are missing."; } @@ -200,9 +200,14 @@ class SchemaValidator } 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(), $assoc['joinColumns'])) . + "however '" . implode(", ", array_diff($class->getIdentifierColumnNames(), $ids)) . "' are missing."; } } From 4571e498b4da9aaa24c27e0a7f4288bb2e182cb6 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 13 Nov 2011 17:16:43 +0100 Subject: [PATCH 68/77] DDC-1477 - Adjust patch to really fix bug in Proxy generation --- lib/Doctrine/ORM/Proxy/ProxyFactory.php | 6 ++++-- .../Doctrine/Tests/ORM/Functional/MappedSuperclassTest.php | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index 0e37b3fac..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")) || $class->reflClass->getName() != $method->class) { + 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 '; @@ -234,7 +236,7 @@ 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" && 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()); From c2bb281e8074248f387cf672cf9357a5e74b2c5f Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 13 Nov 2011 17:44:37 +0100 Subject: [PATCH 69/77] Merge travis support from @pborreli - thanks! --- .travis.yml | 19 +++++++++++++++++ README.markdown | 7 +++++-- tests/travis/mysql.travis.xml | 35 ++++++++++++++++++++++++++++++++ tests/travis/postgres.travis.xml | 34 +++++++++++++++++++++++++++++++ tests/travis/sqlite.travis.xml | 15 ++++++++++++++ 5 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 .travis.yml create mode 100644 tests/travis/mysql.travis.xml create mode 100644 tests/travis/postgres.travis.xml create mode 100644 tests/travis/sqlite.travis.xml 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..dabcc015c 100644 --- a/README.markdown +++ b/README.markdown @@ -1,14 +1,17 @@ # Doctrine 2 ORM +[![Build Status](https://secure.travis-ci.org/doctrine/doctrine2.png)](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/tests/travis/mysql.travis.xml b/tests/travis/mysql.travis.xml new file mode 100644 index 000000000..8812dcad7 --- /dev/null +++ b/tests/travis/mysql.travis.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + ./../Doctrine/Tests/ORM + + + + + performance + locking_functional + + + + + diff --git a/tests/travis/postgres.travis.xml b/tests/travis/postgres.travis.xml new file mode 100644 index 000000000..5f72721d9 --- /dev/null +++ b/tests/travis/postgres.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 From 0fdffb9dbc206945a0f298b1a6c8256d0624296d Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 13 Nov 2011 17:57:54 +0100 Subject: [PATCH 70/77] Fix postgresql travis phpunit configuration file --- tests/travis/{postgres.travis.xml => pgsql.travis.xml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/travis/{postgres.travis.xml => pgsql.travis.xml} (100%) diff --git a/tests/travis/postgres.travis.xml b/tests/travis/pgsql.travis.xml similarity index 100% rename from tests/travis/postgres.travis.xml rename to tests/travis/pgsql.travis.xml From 077d4a0e1587b49e3aa60b98d51520798dff0c5c Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 13 Nov 2011 22:04:48 +0100 Subject: [PATCH 71/77] Fix travis configuration files --- tests/travis/mysql.travis.xml | 47 +++++++++++++++----------------- tests/travis/pgsql.travis.xml | 50 +++++++++++++++++------------------ 2 files changed, 47 insertions(+), 50 deletions(-) diff --git a/tests/travis/mysql.travis.xml b/tests/travis/mysql.travis.xml index 8812dcad7..f17a4b87d 100644 --- a/tests/travis/mysql.travis.xml +++ b/tests/travis/mysql.travis.xml @@ -1,34 +1,31 @@ - - - - - - - - - + + + + + + + - - - - - - - - + + + + + + + - - - ./../Doctrine/Tests/ORM - + + + ./../Doctrine/Tests/ORM + - - performance - locking_functional - + + performance + locking_functional + diff --git a/tests/travis/pgsql.travis.xml b/tests/travis/pgsql.travis.xml index 5f72721d9..fa0581acb 100644 --- a/tests/travis/pgsql.travis.xml +++ b/tests/travis/pgsql.travis.xml @@ -1,34 +1,34 @@ - + - - - - - - + + + + + + - - - - - - - - - - ./../Doctrine/Tests/ORM - - + + + + + + + + + + ./../Doctrine/Tests/ORM + + - - - performance - locking_functional - - + + + performance + locking_functional + + \ No newline at end of file From c87d5af243d17b688f46e5f594ddf85d9cd279d6 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 13 Nov 2011 22:40:27 +0100 Subject: [PATCH 72/77] Specialize build status on versions --- README.markdown | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index dabcc015c..00458ca04 100644 --- a/README.markdown +++ b/README.markdown @@ -1,6 +1,7 @@ # Doctrine 2 ORM -[![Build Status](https://secure.travis-ci.org/doctrine/doctrine2.png)](http://travis-ci.org/doctrine/doctrine2) +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 From c648981f28bd6279ba6dbcfe9ae3adf61dfc6f00 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 13 Nov 2011 23:14:31 +0100 Subject: [PATCH 73/77] DDC-1461 - Verified deferred explicit works --- .../Doctrine/Tests/Models/CMS/CmsAddress.php | 6 +- .../ORM/Functional/Ticket/DDC1461Test.php | 112 ++++++++++++++++++ 2 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1461Test.php diff --git a/tests/Doctrine/Tests/Models/CMS/CmsAddress.php b/tests/Doctrine/Tests/Models/CMS/CmsAddress.php index 9db2b6475..9119a6f58 100644 --- a/tests/Doctrine/Tests/Models/CMS/CmsAddress.php +++ b/tests/Doctrine/Tests/Models/CMS/CmsAddress.php @@ -38,7 +38,7 @@ class CmsAddress public $street; /** - * @OneToOne(targetEntity="CmsUser", inversedBy="address") + * @OneToOne(targetEntity="CmsUser", inversedBy="address", cascade={"persist"}) * @JoinColumn(referencedColumnName="id") */ public $user; @@ -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/ORM/Functional/Ticket/DDC1461Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1461Test.php new file mode 100644 index 000000000..2f59ede23 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1461Test.php @@ -0,0 +1,112 @@ +useModelSet('cms'); + parent::setUp(); + + try { + $this->_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1461TwitterAccount'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1461User') + )); + } catch(\Exception $e) { + + } + } + + public function testChangeDetectionDeferredImplitic() + { + $address = new \Doctrine\Tests\Models\CMS\CmsAddress(); + $address->city = "Karlsruhe"; + $address->country = "Germany"; + $address->street = "somestreet"; + $address->zip = 12345; + + $this->_em->persist($address); + $this->_em->flush(); + + $user = new CmsUser(); + $user->name = "schmittjoh"; + $user->username = "schmittjoh"; + $user->status = "active"; + + $address->setUser($user); + $this->_em->flush(); + $this->_em->clear(); + + $user = $this->_em->find(get_class($user), $user->getId()); + $this->assertNotNull($user->getAddress()); + $this->assertEquals("Karlsruhe", $user->getAddress()->getCity()); + } + + 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 From 81cc6d9da83d217ea62bd467053fd9885d1083cd Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Mon, 14 Nov 2011 01:36:39 -0200 Subject: [PATCH 74/77] Implemented alias support for EntityResult. This addresses DDC-1096 and DDC-1424. Improved DQL Parser, SQL Walker and Hydrators in general. Performance is generally improved by a factor of 20%. There is still more to be done, like remove the isMixed in ResultSetMapping, mainly because this query - SELECT u AS user FROM User u -, it should return an array('user' => [User object]), while currently it doesn't due to this before mentioned 'bug' in RSM. Will open a separate ticket for this. Also, UnitOfWork and Hydrators share code that could be abstracted/improved. --- .gitignore | 3 + .../Internal/Hydration/AbstractHydrator.php | 165 ++-- .../ORM/Internal/Hydration/ArrayHydrator.php | 74 +- .../ORM/Internal/Hydration/ObjectHydrator.php | 122 ++- lib/Doctrine/ORM/Query/Parser.php | 811 ++++++++++-------- lib/Doctrine/ORM/Query/ResultSetMapping.php | 115 +-- lib/Doctrine/ORM/Query/SqlWalker.php | 130 +-- .../Tests/ORM/Hydration/ArrayHydratorTest.php | 459 +++++----- .../ORM/Hydration/ObjectHydratorTest.php | 706 ++++++++------- 9 files changed, 1436 insertions(+), 1149 deletions(-) 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/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php index 493b1461c..146dfb5c5 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php @@ -74,7 +74,7 @@ abstract class AbstractHydrator * * @param object $stmt * @param object $resultSetMapping - * + * * @return IterableResult */ public function iterate($stmt, $resultSetMapping, array $hints = array()) @@ -82,9 +82,9 @@ abstract class AbstractHydrator $this->_stmt = $stmt; $this->_rsm = $resultSetMapping; $this->_hints = $hints; - + $this->prepare(); - + return new IterableResult($this); } @@ -100,13 +100,13 @@ abstract class AbstractHydrator $this->_stmt = $stmt; $this->_rsm = $resultSetMapping; $this->_hints = $hints; - + $this->prepare(); - + $result = $this->hydrateAllData(); - + $this->cleanup(); - + return $result; } @@ -119,17 +119,17 @@ abstract class AbstractHydrator public function hydrateRow() { $row = $this->_stmt->fetch(PDO::FETCH_ASSOC); - + if ( ! $row) { $this->cleanup(); - + return false; } - + $result = array(); - + $this->hydrateRowData($row, $this->_cache, $result); - + return $result; } @@ -147,7 +147,7 @@ abstract class AbstractHydrator protected function cleanup() { $this->_rsm = null; - + $this->_stmt->closeCursor(); $this->_stmt = null; } @@ -195,34 +195,44 @@ abstract class AbstractHydrator 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; } @@ -233,10 +243,10 @@ 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; } @@ -259,7 +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 @@ -267,7 +277,7 @@ abstract class AbstractHydrator * * @param array $data * @param array $cache - * + * * @return array The processed row. */ protected function gatherScalarRowData(&$data, &$cache) @@ -277,48 +287,65 @@ abstract class AbstractHydrator 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; } - + /** * Register entity as managed in UnitOfWork. - * + * * @param Doctrine\ORM\Mapping\ClassMetadata $class * @param object $entity - * @param array $data + * @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) { @@ -338,7 +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 e25df2fea..817e30baf 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php @@ -28,6 +28,11 @@ use PDO, Doctrine\DBAL\Connection, Doctrine\ORM\Mapping\ClassMetadata; * @since 2.0 * @author Roman Borschel * @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 { @@ -39,8 +44,8 @@ class ArrayHydrator extends AbstractHydrator private $_idTemplate = array(); private $_resultCounter = 0; - /** - * {@inheritdoc} + /** + * {@inheritdoc} */ protected function prepare() { @@ -49,7 +54,7 @@ class ArrayHydrator extends AbstractHydrator $this->_resultPointers = array(); $this->_idTemplate = array(); $this->_resultCounter = 0; - + foreach ($this->_rsm->aliasMap as $dqlAlias => $className) { $this->_identifierMap[$dqlAlias] = array(); $this->_resultPointers[$dqlAlias] = array(); @@ -57,14 +62,14 @@ class ArrayHydrator extends AbstractHydrator } } - /** - * {@inheritdoc} + /** + * {@inheritdoc} */ protected function hydrateAllData() { $result = array(); $cache = array(); - + while ($data = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { $this->hydrateRowData($data, $cache, $result); } @@ -85,8 +90,9 @@ class ArrayHydrator extends AbstractHydrator // Extract scalar values. They're appended at the end. if (isset($rowData['scalars'])) { $scalars = $rowData['scalars']; + unset($rowData['scalars']); - + if (empty($rowData)) { ++$this->_resultCounter; } @@ -100,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]) ) { @@ -119,22 +125,23 @@ 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]; // 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])) { @@ -142,15 +149,17 @@ class ArrayHydrator extends AbstractHydrator } 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])) { @@ -166,13 +175,14 @@ 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; } @@ -180,12 +190,12 @@ class ArrayHydrator extends AbstractHydrator ++$this->_resultCounter; continue; } - + // Check for an existing element if ($this->_isSimpleQuery || ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { $element = $rowData[$dqlAlias]; if ($this->_rsm->isMixed) { - $element = array(0 => $element); + $element = array($entityKey => $element); } if (isset($this->_rsm->indexByMap[$dqlAlias])) { @@ -240,37 +250,37 @@ class ArrayHydrator extends AbstractHydrator { if ($coll === null) { unset($this->_resultPointers[$dqlAlias]); // Ticket #1228 - + return; } - + if ($index !== false) { $this->_resultPointers[$dqlAlias] =& $coll[$index]; - + return; - } - + } + if ( ! $coll) { return; } - + if ($oneToOne) { $this->_resultPointers[$dqlAlias] =& $coll; - + return; } - + end($coll); $this->_resultPointers[$dqlAlias] =& $coll[key($coll)]; - + return; } - + /** * Retrieve ClassMetadata associated to entity class name. - * + * * @param string $className - * + * * @return Doctrine\ORM\Mapping\ClassMetadata */ private function getClassMetadata($className) @@ -278,7 +288,7 @@ class ArrayHydrator extends AbstractHydrator 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/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index d7d0281c4..c56b6eb22 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -32,8 +32,13 @@ use PDO, * @since 2.0 * @author Roman Borschel * @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 { @@ -60,52 +65,67 @@ class ObjectHydrator extends AbstractHydrator $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; } } } @@ -120,7 +140,7 @@ class ObjectHydrator extends AbstractHydrator $eagerLoad = (isset($this->_hints['deferEagerLoad'])) && $this->_hints['deferEagerLoad'] == true; parent::cleanup(); - + $this->_identifierMap = $this->_initializedCollections = $this->_existingCollections = @@ -137,7 +157,7 @@ class ObjectHydrator extends AbstractHydrator protected function hydrateAllData() { $result = array(); - $cache = array(); + $cache = array(); while ($row = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { $this->hydrateRowData($row, $cache, $result); @@ -159,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! @@ -203,6 +226,7 @@ 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]]; @@ -211,12 +235,12 @@ class ObjectHydrator extends AbstractHydrator } $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); @@ -226,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 = ''; @@ -257,6 +282,7 @@ class ObjectHydrator extends AbstractHydrator if ( ! isset($this->_ce[$className])) { $this->_ce[$className] = $this->_em->getClassMetadata($className); } + return $this->_ce[$className]; } @@ -387,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 @@ -417,11 +444,12 @@ 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; } @@ -434,7 +462,7 @@ class ObjectHydrator extends AbstractHydrator if ( ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { $element = $this->_getEntity($rowData[$dqlAlias], $dqlAlias); if ($this->_rsm->isMixed) { - $element = array(0 => $element); + $element = array($entityKey => $element); } if (isset($this->_rsm->indexByMap[$dqlAlias])) { 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/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index 5ac99ca82..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) @@ -158,20 +155,27 @@ class ResultSetMapping public function addIndexBy($alias, $fieldName) { $found = false; + foreach ($this->fieldMappings AS $columnName => $columnFieldName) { - if ($columnFieldName === $fieldName && $this->columnOwnerMap[$columnName] == $alias) { - $this->addIndexByColumn($alias, $columnName); - $found = true; - break; - } + 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) { - throw new \LogicException("Cannot add index by for dql alias " . $alias . " and field " . - $fieldName . " without calling addFieldResult() for them before."); + 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); } - */ + */ } /** @@ -244,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; } @@ -260,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. * @@ -275,6 +280,7 @@ class ResultSetMapping public function addScalarResult($columnName, $alias) { $this->scalarMappings[$columnName] = $alias; + if ( ! $this->isMixed && $this->fieldMappings) { $this->isMixed = true; } @@ -282,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 @@ -421,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 @@ -434,6 +440,7 @@ class ResultSetMapping { $this->metaMappings[$columnName] = $fieldName; $this->columnOwnerMap[$columnName] = $alias; + if ($isIdentifierColumn) { $this->isIdentifierColumn[$alias][$columnName] = true; } diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 03933d3d9..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 @@ -257,13 +258,13 @@ class SqlWalker implements TreeWalker // 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 '; - + $sqlParts = array(); foreach ($class->getQuotedIdentifierColumnNames($this->_platform) as $columnName) { $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; } - + $sql .= implode(' AND ', $sqlParts); } @@ -271,7 +272,7 @@ class SqlWalker implements TreeWalker if ($this->_query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) { return $sql; } - + // LEFT JOIN child class tables foreach ($class->subClasses as $subClassName) { $subClass = $this->_em->getClassMetadata($subClassName); @@ -294,18 +295,19 @@ class SqlWalker implements TreeWalker private function _generateOrderedCollectionOrderByItems() { $sqlParts = array(); - - foreach ($this->_selectedClasses AS $dqlAlias => $class) { - $qComp = $this->_queryComponents[$dqlAlias]; + + 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($class->name)->getOwningTable($fieldName) + ? $this->_em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name)->getOwningTable($fieldName) : $qComp['metadata']->getTableName(); - + $sqlParts[] = $this->getSQLTableAlias($tableName, $dqlAlias) . '.' . $columnName . ' ' . $orientation; } } @@ -327,7 +329,7 @@ class SqlWalker implements TreeWalker $class = $this->_queryComponents[$dqlAlias]['metadata']; if ( ! $class->isInheritanceTypeSingleTable()) continue; - + $conn = $this->_em->getConnection(); $values = array(); @@ -344,7 +346,7 @@ class SqlWalker implements TreeWalker } $sql = implode(' AND ', $sqlParts); - + return (count($sqlParts) > 1) ? '(' . $sql . ')' : $sql; } @@ -376,19 +378,19 @@ class SqlWalker implements TreeWalker 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 $class) { + foreach ($this->_selectedClasses AS $selectedClass) { if ( ! $class->isVersioned) { - throw \Doctrine\ORM\OptimisticLockException::lockFailed($class->name); + throw \Doctrine\ORM\OptimisticLockException::lockFailed($selectedClass['class']->name); } } break; - + default: throw \Doctrine\ORM\Query\QueryException::invalidLockMode(); } @@ -521,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'] ); @@ -545,10 +552,10 @@ class SqlWalker implements TreeWalker $this->_rsm->setDiscriminatorColumn($dqlAlias, $columnAlias); $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $discrColumn['fieldName']); } - + // 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; @@ -564,7 +571,7 @@ class SqlWalker implements TreeWalker $this->_rsm->addMetaResult($dqlAlias, $columnAlias, $srcColumn, (isset($assoc['id']) && $assoc['id'] === true)); } } - + // Add foreign key columns of subclasses foreach ($class->subClasses as $subClassName) { $subClass = $this->_em->getClassMetadata($subClassName); @@ -573,12 +580,12 @@ class SqlWalker implements TreeWalker 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); @@ -661,11 +668,11 @@ class SqlWalker implements TreeWalker public function walkOrderByClause($orderByClause) { $orderByItems = array_map(array($this, 'walkOrderByItem'), $orderByClause->orderByItems); - + if (($collectionOrderByItems = $this->_generateOrderedCollectionOrderByItems()) !== '') { $orderByItems = array_merge($orderByItems, (array) $collectionOrderByItems); } - + return ' ORDER BY ' . implode(', ', $orderByItems); } @@ -709,7 +716,7 @@ class SqlWalker implements TreeWalker $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. $this->_rsm->addIndexBy( @@ -995,7 +1002,7 @@ class SqlWalker implements TreeWalker $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); $columnName = $class->getQuotedColumnName($fieldName, $this->_platform); $columnAlias = $this->getSQLColumnAlias($columnName); - + $sql .= $sqlTableAlias . '.' . $columnName . ' AS ' . $columnAlias; if ( ! $hidden) { @@ -1003,7 +1010,7 @@ class SqlWalker implements TreeWalker $this->_scalarFields[$dqlAlias][$fieldName] = $columnAlias; } break; - + case ($expr instanceof AST\AggregateExpression): case ($expr instanceof AST\Functions\FunctionNode): case ($expr instanceof AST\SimpleArithmeticExpression): @@ -1017,29 +1024,29 @@ class SqlWalker implements TreeWalker case ($expr instanceof AST\SimpleCaseExpression): $columnAlias = $this->getSQLColumnAlias('sclr'); $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->_scalarResultCounter++; - + $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; - + $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; - + + $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) { @@ -1050,18 +1057,23 @@ class SqlWalker implements TreeWalker $partialFieldSet = array(); } - $queryComp = $this->_queryComponents[$dqlAlias]; - $class = $queryComp['metadata']; + $queryComp = $this->_queryComponents[$dqlAlias]; + $class = $queryComp['metadata']; + $resultAlias = $selectExpression->fieldIdentificationVariable ?: null; if ( ! isset($this->_selectedClasses[$dqlAlias])) { - $this->_selectedClasses[$dqlAlias] = $class; + $this->_selectedClasses[$dqlAlias] = array( + 'class' => $class, + 'dqlAlias' => $dqlAlias, + 'resultAlias' => $resultAlias + ); } - $beginning = true; + $sqlParts = array(); // Select all fields from the queried class foreach ($class->fieldMappings as $fieldName => $mapping) { - if ($partialFieldSet && !in_array($fieldName, $partialFieldSet)) { + if ($partialFieldSet && ! in_array($fieldName, $partialFieldSet)) { continue; } @@ -1069,12 +1081,11 @@ class SqlWalker implements TreeWalker ? $this->_em->getClassMetadata($mapping['inherited'])->getTableName() : $class->getTableName(); - if ($beginning) $beginning = false; else $sql .= ', '; + $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); + $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); + $quotedColumnName = $class->getQuotedColumnName($fieldName, $this->_platform); - $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); - $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); - $sql .= $sqlTableAlias . '.' . $class->getQuotedColumnName($fieldName, $this->_platform) - . ' AS ' . $columnAlias; + $sqlParts[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS '. $columnAlias; $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); } @@ -1085,7 +1096,7 @@ class SqlWalker implements TreeWalker // 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); + $subClass = $this->_em->getClassMetadata($subClassName); $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); foreach ($subClass->fieldMappings as $fieldName => $mapping) { @@ -1093,16 +1104,17 @@ class SqlWalker implements TreeWalker continue; } - if ($beginning) $beginning = false; else $sql .= ', '; + $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); + $quotedColumnName = $subClass->getQuotedColumnName($fieldName, $this->_platform); - $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); - $sql .= $sqlTableAlias . '.' . $subClass->getQuotedColumnName($fieldName, $this->_platform) - . ' AS ' . $columnAlias; + $sqlParts[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); } } } + + $sql .= implode(', ', $sqlParts); } return $sql; @@ -1116,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) . ')'; } /** @@ -1128,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; @@ -1162,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) . ' ' diff --git a/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php index 616f19146..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,28 +269,28 @@ 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[1])); $this->assertTrue(is_array($result[2])); @@ -262,14 +298,17 @@ class ArrayHydratorTest extends HydrationTestCase // test the scalar values $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[1][0]['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[2][0]['phonenumbers'])); + $this->assertEquals(1, count($result[2][$userEntityKey]['phonenumbers'])); + // test the custom indexing of the phonenumbers - $this->assertTrue(isset($result[1][0]['phonenumbers']['42'])); - $this->assertTrue(isset($result[1][0]['phonenumbers']['43'])); - $this->assertTrue(isset($result[2][0]['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,19 +867,24 @@ 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() + public function testIndexByAndMixedResult($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'); @@ -837,23 +897,24 @@ class ArrayHydratorTest extends HydrationTestCase 'u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', '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(2, count($result)); + $this->assertTrue(isset($result[1])); - $this->assertEquals(1, $result[1][0]['id']); + $this->assertEquals(1, $result[1][$userEntityKey]['id']); + $this->assertTrue(isset($result[2])); - $this->assertEquals(2, $result[2][0]['id']); + $this->assertEquals(2, $result[2][$userEntityKey]['id']); } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php index 268118d72..c3d6a9412 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php @@ -15,14 +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 PARTIAL u.{id,name} + * SELECT PARTIAL u.{id,name} * FROM Doctrine\Tests\Models\CMS\CmsUser u + * + * @dataProvider provideDataForUserEntityResult */ - public function testSimpleEntityScalarFieldsQuery() + 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'); @@ -43,26 +71,28 @@ class ObjectHydratorTest extends HydrationTestCase $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} + * 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'); @@ -83,17 +113,18 @@ class ObjectHydratorTest extends HydrationTestCase } /** - * SELECT u.id, - * u.name + * SELECT u.id, u.name * FROM Doctrine\Tests\Models\CMS\CmsUser u + * + * @dataProvider provideDataForUserEntityResult */ - public function testScalarQueryWithoutResultVariables() + public function testScalarQueryWithoutResultVariables($userEntityKey) { $rsm = new ResultSetMapping; - $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addScalarResult('sclr0', 'id'); $rsm->addScalarResult('sclr1', 'name'); - + // Faked result set $resultSet = array( array( @@ -111,27 +142,28 @@ class ObjectHydratorTest extends HydrationTestCase $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 + * 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'); @@ -166,22 +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 + * 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'); @@ -215,21 +252,21 @@ class ObjectHydratorTest extends HydrationTestCase $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(1, count($result)); - + $this->assertInstanceOf('Doctrine\Tests\Models\ECommerce\ECommerceProduct', $result[0]); } /** - * SELECT PARTIAL u.{id, status}, - * PARTIAL p.{phonenumber}, - * UPPER(u.name) nameUpper - * FROM User u + * 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', @@ -269,42 +306,44 @@ class ObjectHydratorTest extends HydrationTestCase $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); - + $this->assertInternalType('array', $result); $this->assertInternalType('array', $result[0]); $this->assertInternalType('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->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 PARTIAL u.{id, status}, - * COUNT(p.phonenumber) numPhones + * SELECT PARTIAL u.{id, status}, COUNT(p.phonenumber) numPhones * FROM User u - * JOIN u.phonenumbers p + * 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'); @@ -329,33 +368,33 @@ class ObjectHydratorTest extends HydrationTestCase $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); - + $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, - * p, - * UPPER(u.name) nameUpper - * FROM User u + * SELECT u, p, UPPER(u.name) nameUpper + * FROM User u * INDEX BY u.id - * JOIN u.phonenumbers p + * 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', @@ -398,7 +437,7 @@ class ObjectHydratorTest extends HydrationTestCase $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); - + $this->assertInternalType('array', $result); $this->assertInternalType('array', $result[1]); $this->assertInternalType('array', $result[2]); @@ -407,43 +446,45 @@ class ObjectHydratorTest extends HydrationTestCase $this->assertEquals('ROMANB', $result[1]['nameUpper']); $this->assertEquals('JWAGE', $result[2]['nameUpper']); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[1][0]); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[2][0]); - $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $result[1][0]->phonenumbers); - + $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); + // first user => 2 phonenumbers. notice the custom indexing by user id - $this->assertEquals(2, count($result[1][0]->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[2][0]->phonenumbers)); - + $this->assertEquals(1, count($result[2][$userEntityKey]->phonenumbers)); + // test the custom indexing of the phonenumbers - $this->assertTrue(isset($result[1][0]->phonenumbers['42'])); - $this->assertTrue(isset($result[1][0]->phonenumbers['43'])); - $this->assertTrue(isset($result[2][0]->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, p, upper(u.name) nameUpper, a - * from User u - * join u.phonenumbers p - * join u.articles a + * 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'); @@ -462,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', @@ -478,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', @@ -494,70 +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, p, upper(u.name) nameUpper, a, c - * c.id, c.topic - * from User u - * join u.phonenumbers p - * join u.articles a - * left join a.comments c + * 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'); @@ -580,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', @@ -590,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', @@ -600,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', @@ -610,7 +653,7 @@ class ObjectHydratorTest extends HydrationTestCase 'a__topic' => 'ZendCon', 'c__id' => null, 'c__topic' => null - ), + ), array( 'u__id' => '2', 'u__status' => 'developer', @@ -620,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', @@ -630,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)); } /** @@ -692,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'); @@ -712,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', @@ -728,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'); @@ -790,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'); @@ -839,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', @@ -847,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->assertEquals(1, count($result)); - $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $result[0][0]); // User object - $this->assertEquals(1, $result[0]['id']); + $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'); @@ -900,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); @@ -924,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'); @@ -955,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'); @@ -1066,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."); @@ -1096,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'); @@ -1129,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'); @@ -1199,23 +1271,28 @@ class ObjectHydratorTest extends HydrationTestCase ), ); - $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() + public function testIndexByAndMixedResult($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'); @@ -1236,24 +1313,30 @@ class ObjectHydratorTest extends HydrationTestCase ), ); - $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(isset($result[1])); - $this->assertEquals(1, $result[1][0]->id); + $this->assertEquals(1, $result[1][$userEntityKey]->id); + $this->assertTrue(isset($result[2])); - $this->assertEquals(2, $result[2][0]->id); + $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() + public function testIndexByScalarsOnly($userEntityKey) { $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u', $userEntityKey ?: null); $rsm->addScalarResult('sclr0', 'nameUpper'); $rsm->addIndexByScalar('sclr0'); @@ -1270,9 +1353,14 @@ class ObjectHydratorTest extends HydrationTestCase $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); + $this->assertEquals( + array( + 'ROMANB' => array('nameUpper' => 'ROMANB'), + 'JWAGE' => array('nameUpper' => 'JWAGE') + ), + $result + ); } } From 0632b3749272d9b0784f4d3e2479baba97410062 Mon Sep 17 00:00:00 2001 From: "Fabio B. Silva" Date: Mon, 14 Nov 2011 13:15:43 -0200 Subject: [PATCH 75/77] fix default field type --- lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 12 ++- .../ORM/Mapping/Driver/YamlDriver.php | 35 ++++----- .../DDC1476EntityWithDefaultFieldType.php | 76 +++++++++++++++++++ .../ORM/Mapping/AbstractMappingDriverTest.php | 45 +++++++++++ ...1476.DDC1476EntityWithDefaultFieldType.php | 12 +++ ....DDC1476EntityWithDefaultFieldType.dcm.xml | 15 ++++ ....DDC1476EntityWithDefaultFieldType.dcm.yml | 8 ++ 7 files changed, 183 insertions(+), 20 deletions(-) create mode 100644 tests/Doctrine/Tests/Models/DDC1476/DDC1476EntityWithDefaultFieldType.php create mode 100644 tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.DDC1476.DDC1476EntityWithDefaultFieldType.php create mode 100644 tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.DDC1476.DDC1476EntityWithDefaultFieldType.dcm.xml create mode 100644 tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.DDC1476.DDC1476EntityWithDefaultFieldType.dcm.yml diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index fd6bf081a..2b7657763 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -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']; 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/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/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/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 From 8af0f9d0711a7dcba8364e86c29cc3cc6e426010 Mon Sep 17 00:00:00 2001 From: "Fabio B. Silva" Date: Mon, 14 Nov 2011 16:07:37 -0200 Subject: [PATCH 76/77] added support for Inherited Named Queries --- .../ORM/Mapping/ClassMetadataFactory.php | 23 ++++ .../ORM/Mapping/ClassMetadataInfo.php | 13 +- .../ORM/Functional/Ticket/DDC1404Test.php | 129 ++++++++++++++++++ .../Tests/ORM/Mapping/ClassMetadataTest.php | 2 +- 4 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1404Test.php 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 592367ab7..b8c4ef2f1 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -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']; } /** @@ -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 + ); } /** 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/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() From 909504c0743c8344173ac8c208989baaa4836b34 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 14 Nov 2011 21:05:44 +0100 Subject: [PATCH 77/77] DDC-1461 - Fix test failures --- .../Doctrine/Tests/Models/CMS/CmsAddress.php | 2 +- .../ORM/Functional/Ticket/DDC1461Test.php | 28 +------------------ 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/tests/Doctrine/Tests/Models/CMS/CmsAddress.php b/tests/Doctrine/Tests/Models/CMS/CmsAddress.php index 9119a6f58..d32416a5e 100644 --- a/tests/Doctrine/Tests/Models/CMS/CmsAddress.php +++ b/tests/Doctrine/Tests/Models/CMS/CmsAddress.php @@ -38,7 +38,7 @@ class CmsAddress public $street; /** - * @OneToOne(targetEntity="CmsUser", inversedBy="address", cascade={"persist"}) + * @OneToOne(targetEntity="CmsUser", inversedBy="address") * @JoinColumn(referencedColumnName="id") */ public $user; diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1461Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1461Test.php index 2f59ede23..a47571a75 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1461Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1461Test.php @@ -14,7 +14,6 @@ class DDC1461Test extends \Doctrine\Tests\OrmFunctionalTestCase { public function setUp() { - $this->useModelSet('cms'); parent::setUp(); try { @@ -26,32 +25,7 @@ class DDC1461Test extends \Doctrine\Tests\OrmFunctionalTestCase } } - - public function testChangeDetectionDeferredImplitic() - { - $address = new \Doctrine\Tests\Models\CMS\CmsAddress(); - $address->city = "Karlsruhe"; - $address->country = "Germany"; - $address->street = "somestreet"; - $address->zip = 12345; - - $this->_em->persist($address); - $this->_em->flush(); - - $user = new CmsUser(); - $user->name = "schmittjoh"; - $user->username = "schmittjoh"; - $user->status = "active"; - - $address->setUser($user); - $this->_em->flush(); - $this->_em->clear(); - - $user = $this->_em->find(get_class($user), $user->getId()); - $this->assertNotNull($user->getAddress()); - $this->assertEquals("Karlsruhe", $user->getAddress()->getCity()); - } - + public function testChangeDetectionDeferredExplicit() { $user = new DDC1461User;