Skip to content

Commit

Permalink
(#4687) Support for nested "new" operators
Browse files Browse the repository at this point in the history
  • Loading branch information
fesor committed Jan 27, 2018
1 parent ee4e267 commit 6718d89
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 7 deletions.
23 changes: 23 additions & 0 deletions lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,29 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon
}
}

foreach ($this->rsm->nestedNewObjectArguments as $objIndex => ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex]) {
$newObject = $rowData['newObjects'][$objIndex];
unset($rowData['newObjects'][$objIndex]);

$class = $newObject['class'];
$args = $newObject['args'];
$obj = $class->newInstanceArgs($args);

$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $obj;
}


if (isset($rowData['newObjects'])) {
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
$class = $newObject['class'];
$args = $newObject['args'];
$obj = $class->newInstanceArgs($args);

$rowData['newObjects'][$objIndex]['obj'] = $obj;
}
}


return $rowData;
}

Expand Down
5 changes: 2 additions & 3 deletions lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,8 @@ protected function hydrateRowData(array $row, array &$result)
$onlyOneRootAlias = $scalarCount === 0 && count($rowData['newObjects']) === 1;

foreach ($rowData['newObjects'] as $objIndex => $newObject) {
$class = $newObject['class'];
$args = $newObject['args'];
$obj = $class->newInstanceArgs($args);
$args = $newObject['args'];
$obj = $newObject['obj'];

if ($onlyOneRootAlias || \count($args) === $scalarCount) {
$result[$resultKey] = $obj;
Expand Down
5 changes: 2 additions & 3 deletions lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -532,12 +532,11 @@ protected function hydrateRowData(array $row, array &$result)
$resultKey = $this->resultCounter - 1;
}


$hasNoScalars = ! (isset($rowData['scalars']) && $rowData['scalars']);

foreach ($rowData['newObjects'] as $objIndex => $newObject) {
$class = $newObject['class'];
$args = $newObject['args'];
$obj = $class->newInstanceArgs($args);
$obj = $newObject['obj'];

if ($hasNoScalars && \count($rowData['newObjects']) === 1) {
$result[$resultKey] = $obj;
Expand Down
6 changes: 6 additions & 0 deletions lib/Doctrine/ORM/Query/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -1873,6 +1873,12 @@ public function NewObjectArg()
return $expression;
}

if ($token['type'] === Lexer::T_NEW) {
$expression = $this->NewObjectExpression();

return $expression;
}

return $this->ScalarExpression();
}

Expand Down
26 changes: 26 additions & 0 deletions lib/Doctrine/ORM/Query/ResultSetMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ class ResultSetMapping
*/
public $newObjectMappings = [];

/**
* Maps last argument for new objects in order to initiate object construction
*
* @var array
*/
public $nestedNewObjectArguments = [];

/**
* Maps metadata parameter names to the metadata attribute.
*
Expand Down Expand Up @@ -555,4 +562,23 @@ public function addMetaResult($alias, $columnName, $fieldName, $isIdentifierColu

return $this;
}

public function addNewObjectAsArgument($alias, $objOwner, $objOwnerIdx)
{
$owner = [
'ownerIndex' => $objOwner,
'argIndex' => $objOwnerIdx,
];

if (!isset($this->nestedNewObjectArguments[$owner['ownerIndex']])) {
$this->nestedNewObjectArguments[$alias] = $owner;

return;
}

$this->nestedNewObjectArguments = array_merge(
[$alias => $owner],
$this->nestedNewObjectArguments
);
}
}
23 changes: 22 additions & 1 deletion lib/Doctrine/ORM/Query/SqlWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ class SqlWalker implements TreeWalker
*/
private $newObjectCounter = 0;

/**
* Contains nesting levels of new objects arguments
*
* @var array of newObject indexes
*/
private $newObjectStack = [];

/**
* @var ParserResult
*/
Expand Down Expand Up @@ -1578,7 +1585,14 @@ public function walkParenthesisExpression(AST\ParenthesisExpression $parenthesis
public function walkNewObject($newObjectExpression, $newObjectResultAlias = null)
{
$sqlSelectExpressions = [];
$objIndex = $newObjectResultAlias ?: $this->newObjectCounter++;

$objOwner = $objOwnerIdx = null;
if (!empty($this->newObjectStack)) {
[$objOwner, $objOwnerIdx] = end($this->newObjectStack);
$objIndex = "$objOwner:$objOwnerIdx";
} else {
$objIndex = $newObjectResultAlias ?: $this->newObjectCounter++;
}

foreach ($newObjectExpression->args as $argIndex => $e) {
$resultAlias = $this->scalarResultCounter++;
Expand All @@ -1587,7 +1601,10 @@ public function walkNewObject($newObjectExpression, $newObjectResultAlias = null

switch (true) {
case ($e instanceof AST\NewObjectExpression):
array_push($this->newObjectStack, [$objIndex, $argIndex]);
$sqlSelectExpressions[] = $e->dispatch($this);
array_pop($this->newObjectStack);

break;

case ($e instanceof AST\Subselect):
Expand Down Expand Up @@ -1630,6 +1647,10 @@ public function walkNewObject($newObjectExpression, $newObjectResultAlias = null
'objIndex' => $objIndex,
'argIndex' => $argIndex,
];

if ($objOwner) {
$this->rsm->addNewObjectAsArgument($objIndex, $objOwner, $objOwnerIdx);
}
}

return implode(', ', $sqlSelectExpressions);
Expand Down
73 changes: 73 additions & 0 deletions tests/Doctrine/Tests/ORM/Functional/NewOperatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,79 @@ public function testClassCantBeInstantiatedException()
$dql = "SELECT new Doctrine\Tests\ORM\Functional\ClassWithPrivateConstructor(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u";
$this->em->createQuery($dql)->getResult();
}

public function testShouldSupportNestedNewOperators()
{
$dql = "
SELECT
new CmsUserDTO(
u.name,
e.email,
new CmsAddressDTO(
a.country,
a.city,
new CmsAddressDTO(
a.country,
a.city
)
)
) as user,
u.status,
u.username as cmsUserUsername
FROM
Doctrine\Tests\Models\CMS\CmsUser u
JOIN
u.email e
JOIN
u.address a
ORDER BY
u.name";

$query = $this->getEntityManager()->createQuery($dql);
$result = $query->getResult();

$this->assertCount(3, $result);

$this->assertInstanceOf(CmsUserDTO::class, $result[0]['user']);
$this->assertInstanceOf(CmsUserDTO::class, $result[1]['user']);
$this->assertInstanceOf(CmsUserDTO::class, $result[2]['user']);

$this->assertInstanceOf(CmsAddressDTO::class, $result[0]['user']->address);
$this->assertInstanceOf(CmsAddressDTO::class, $result[1]['user']->address);
$this->assertInstanceOf(CmsAddressDTO::class, $result[2]['user']->address);

$this->assertEquals($this->fixtures[0]->name, $result[0]['user']->name);
$this->assertEquals($this->fixtures[1]->name, $result[1]['user']->name);
$this->assertEquals($this->fixtures[2]->name, $result[2]['user']->name);

$this->assertEquals($this->fixtures[0]->email->email, $result[0]['user']->email);
$this->assertEquals($this->fixtures[1]->email->email, $result[1]['user']->email);
$this->assertEquals($this->fixtures[2]->email->email, $result[2]['user']->email);

$this->assertEquals($this->fixtures[0]->address->city, $result[0]['user']->address->city);
$this->assertEquals($this->fixtures[1]->address->city, $result[1]['user']->address->city);
$this->assertEquals($this->fixtures[2]->address->city, $result[2]['user']->address->city);

$this->assertEquals($this->fixtures[0]->address->country, $result[0]['user']->address->country);
$this->assertEquals($this->fixtures[1]->address->country, $result[1]['user']->address->country);
$this->assertEquals($this->fixtures[2]->address->country, $result[2]['user']->address->country);

$this->assertEquals($this->fixtures[0]->status,$result[0]['status']);
$this->assertEquals($this->fixtures[1]->status,$result[1]['status']);
$this->assertEquals($this->fixtures[2]->status,$result[2]['status']);

$this->assertEquals($this->fixtures[0]->username,$result[0]['cmsUserUsername']);
$this->assertEquals($this->fixtures[1]->username,$result[1]['cmsUserUsername']);
$this->assertEquals($this->fixtures[2]->username,$result[2]['cmsUserUsername']);
}

private function dumpResultSetMapping(Query $query)
{
$rsm = (\Closure::bind(function ($q) {
return $q->getResultSetMapping();
}, null, Query::class))($query);
echo json_encode(get_object_vars($rsm), JSON_PRETTY_PRINT);
}
}

class ClassWithTooMuchArgs
Expand Down
17 changes: 17 additions & 0 deletions tests/Doctrine/Tests/ORM/Hydration/ResultSetMappingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,21 @@ public function testIndexByMetadataColumn()

self::assertTrue($this->rsm->hasIndexBy('lu'));
}

public function testNewObjectNestedArgumentsDeepestLeavesShouldComeFirst()
{
$this->rsm->addNewObjectAsArgument('objALevel2', 'objALevel1', 0);
$this->rsm->addNewObjectAsArgument('objALevel3', 'objALevel2', 1);
$this->rsm->addNewObjectAsArgument('objBLevel3', 'objBLevel2', 0);
$this->rsm->addNewObjectAsArgument('objBLevel2', 'objBLevel1', 1);

$expectedArgumentMapping = [
'objALevel3' => ['ownerIndex' => 'objALevel2', 'argIndex' => 1],
'objALevel2' => ['ownerIndex' => 'objALevel1', 'argIndex' => 0],
'objBLevel3' => ['ownerIndex' => 'objBLevel2', 'argIndex' => 0],
'objBLevel2' => ['ownerIndex' => 'objBLevel1', 'argIndex' => 1],
];

$this->assertEquals($expectedArgumentMapping, $this->rsm->nestedNewObjectArguments);
}
}

0 comments on commit 6718d89

Please sign in to comment.