Skip to content

Commit 1eaacf2

Browse files
authored
fix(doctrine): do not apply order extension if sorting already defined (api-platform#4493)
* Do not apply order extension if sorting already applied * fix(doctrine): add missing prophecy call for orm queryBuilder in tests * fix(phpcs): fix cs * fix(tests): remove shouldBeCalled on aggregationBuilder willThrow OutOfRangeException
1 parent 5272238 commit 1eaacf2

File tree

4 files changed

+77
-0
lines changed

4 files changed

+77
-0
lines changed

src/Bridge/Doctrine/MongoDbOdm/Extension/OrderExtension.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\PropertyHelperTrait as MongoDbOdmPropertyHelperTrait;
1818
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1919
use Doctrine\ODM\MongoDB\Aggregation\Builder;
20+
use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort;
2021
use Doctrine\Persistence\ManagerRegistry;
22+
use OutOfRangeException;
2123

2224
/**
2325
* Applies selected ordering while querying resource collection.
@@ -50,6 +52,11 @@ public function __construct(string $order = null, ResourceMetadataFactoryInterfa
5052
*/
5153
public function applyToCollection(Builder $aggregationBuilder, string $resourceClass, string $operationName = null, array &$context = [])
5254
{
55+
// Do not apply order if already defined on $aggregationBuilder
56+
if ($this->hasSortStage($aggregationBuilder)) {
57+
return;
58+
}
59+
5360
$classMetaData = $this->getClassMetadata($resourceClass);
5461
$identifiers = $classMetaData->getIdentifier();
5562
if (null !== $this->resourceMetadataFactory) {
@@ -91,4 +98,27 @@ protected function getManagerRegistry(): ManagerRegistry
9198
{
9299
return $this->managerRegistry;
93100
}
101+
102+
private function hasSortStage(Builder $aggregationBuilder): bool
103+
{
104+
$shouldStop = false;
105+
$index = 0;
106+
107+
do {
108+
try {
109+
if ($aggregationBuilder->getStage($index) instanceof Sort) {
110+
// If at least one stage is sort, then it has sorting
111+
return true;
112+
}
113+
} catch (OutOfRangeException $outOfRangeException) {
114+
// There is no more stages on the aggregation builder
115+
$shouldStop = true;
116+
}
117+
118+
++$index;
119+
} while (!$shouldStop);
120+
121+
// No stage was sort, and we iterated through all stages
122+
return false;
123+
}
94124
}

src/Bridge/Doctrine/Orm/Extension/OrderExtension.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
4646
throw new InvalidArgumentException('The "$resourceClass" parameter must not be null');
4747
}
4848

49+
// Do not apply order if already defined on queryBuilder
50+
$orderByDqlPart = $queryBuilder->getDQLPart('orderBy');
51+
if (\is_array($orderByDqlPart) && \count($orderByDqlPart) > 0) {
52+
return;
53+
}
54+
4955
$rootAlias = $queryBuilder->getRootAliases()[0];
5056

5157
$classMetaData = $queryBuilder->getEntityManager()->getClassMetadata($resourceClass);

tests/Bridge/Doctrine/MongoDbOdm/Extension/OrderExtensionTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
use ApiPlatform\Core\Tests\ProphecyTrait;
2121
use Doctrine\ODM\MongoDB\Aggregation\Builder;
2222
use Doctrine\ODM\MongoDB\Aggregation\Stage\Lookup;
23+
use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort;
2324
use Doctrine\ODM\MongoDB\DocumentManager;
2425
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
2526
use Doctrine\Persistence\ManagerRegistry;
27+
use OutOfRangeException;
2628
use PHPUnit\Framework\TestCase;
2729

2830
/**
@@ -38,6 +40,7 @@ public function testApplyToCollectionWithValidOrder()
3840
{
3941
$aggregationBuilderProphecy = $this->prophesize(Builder::class);
4042

43+
$aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message'));
4144
$aggregationBuilderProphecy->sort(['name' => 'asc'])->shouldBeCalled();
4245

4346
$classMetadataProphecy = $this->prophesize(ClassMetadata::class);
@@ -58,6 +61,7 @@ public function testApplyToCollectionWithWrongOrder()
5861
{
5962
$aggregationBuilderProphecy = $this->prophesize(Builder::class);
6063

64+
$aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message'));
6165
$aggregationBuilderProphecy->sort(['name' => 'asc'])->shouldNotBeCalled();
6266

6367
$classMetadataProphecy = $this->prophesize(ClassMetadata::class);
@@ -79,6 +83,7 @@ public function testApplyToCollectionWithOrderOverridden()
7983
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
8084
$aggregationBuilderProphecy = $this->prophesize(Builder::class);
8185

86+
$aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message'));
8287
$aggregationBuilderProphecy->sort(['foo' => 'DESC'])->shouldBeCalled();
8388

8489
$classMetadataProphecy = $this->prophesize(ClassMetadata::class);
@@ -102,6 +107,7 @@ public function testApplyToCollectionWithOrderOverriddenWithNoDirection()
102107
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
103108
$aggregationBuilderProphecy = $this->prophesize(Builder::class);
104109

110+
$aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message'));
105111
$aggregationBuilderProphecy->sort(['foo' => 'ASC'])->shouldBeCalled();
106112
$aggregationBuilderProphecy->sort(['foo' => 'ASC', 'bar' => 'DESC'])->shouldBeCalled();
107113

@@ -132,6 +138,7 @@ public function testApplyToCollectionWithOrderOverriddenWithAssociation()
132138
$lookupProphecy->alias('author_lkup')->shouldBeCalled();
133139
$aggregationBuilderProphecy->lookup(Dummy::class)->shouldBeCalled()->willReturn($lookupProphecy->reveal());
134140
$aggregationBuilderProphecy->unwind('$author_lkup')->shouldBeCalled();
141+
$aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message'));
135142
$aggregationBuilderProphecy->sort(['author_lkup.name' => 'ASC'])->shouldBeCalled();
136143

137144
$classMetadataProphecy = $this->prophesize(ClassMetadata::class);
@@ -154,4 +161,18 @@ public function testApplyToCollectionWithOrderOverriddenWithAssociation()
154161
$orderExtensionTest = new OrderExtension('asc', $resourceMetadataFactoryProphecy->reveal(), $managerRegistryProphecy->reveal());
155162
$orderExtensionTest->applyToCollection($aggregationBuilder, Dummy::class);
156163
}
164+
165+
public function testApplyToCollectionWithExistingSortStage()
166+
{
167+
$aggregationBuilderProphecy = $this->prophesize(Builder::class);
168+
169+
$aggregationBuilderProphecy->sort(['name' => 'asc'])->shouldNotBeCalled();
170+
$aggregationBuilderProphecy->getStage(0)->shouldBeCalled()->willReturn(new Sort($aggregationBuilder = $aggregationBuilderProphecy->reveal(), 'field'));
171+
172+
$managerRegistryProphecy = $this->prophesize(ManagerRegistry::class);
173+
$managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldNotBeCalled();
174+
175+
$orderExtensionTest = new OrderExtension('asc', null, $managerRegistryProphecy->reveal());
176+
$orderExtensionTest->applyToCollection($aggregationBuilder, Dummy::class);
177+
}
157178
}

tests/Bridge/Doctrine/Orm/Extension/OrderExtensionTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use ApiPlatform\Core\Tests\ProphecyTrait;
2323
use Doctrine\ORM\EntityManager;
2424
use Doctrine\ORM\Mapping\ClassMetadata;
25+
use Doctrine\ORM\Query\Expr\OrderBy;
2526
use Doctrine\ORM\QueryBuilder;
2627
use PHPUnit\Framework\TestCase;
2728

@@ -36,6 +37,7 @@ public function testApplyToCollectionWithValidOrder()
3637
{
3738
$queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
3839

40+
$queryBuilderProphecy->getDQLPart('orderBy')->shouldBeCalled()->willReturn([]);
3941
$queryBuilderProphecy->addOrderBy('o.name', 'asc')->shouldBeCalled();
4042

4143
$classMetadataProphecy = $this->prophesize(ClassMetadata::class);
@@ -56,6 +58,7 @@ public function testApplyToCollectionWithWrongOrder()
5658
{
5759
$queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
5860

61+
$queryBuilderProphecy->getDQLPart('orderBy')->shouldBeCalled()->willReturn([]);
5962
$queryBuilderProphecy->addOrderBy('o.name', 'asc')->shouldNotBeCalled();
6063

6164
$classMetadataProphecy = $this->prophesize(ClassMetadata::class);
@@ -77,6 +80,7 @@ public function testApplyToCollectionWithOrderOverridden()
7780
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
7881
$queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
7982

83+
$queryBuilderProphecy->getDQLPart('orderBy')->shouldBeCalled()->willReturn([]);
8084
$queryBuilderProphecy->addOrderBy('o.foo', 'DESC')->shouldBeCalled();
8185

8286
$classMetadataProphecy = $this->prophesize(ClassMetadata::class);
@@ -100,6 +104,7 @@ public function testApplyToCollectionWithOrderOverriddenWithNoDirection()
100104
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
101105
$queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
102106

107+
$queryBuilderProphecy->getDQLPart('orderBy')->shouldBeCalled()->willReturn([]);
103108
$queryBuilderProphecy->addOrderBy('o.foo', 'ASC')->shouldBeCalled();
104109
$queryBuilderProphecy->addOrderBy('o.bar', 'DESC')->shouldBeCalled();
105110

@@ -124,6 +129,7 @@ public function testApplyToCollectionWithOrderOverriddenWithAssociation()
124129
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
125130
$queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
126131

132+
$queryBuilderProphecy->getDQLPart('orderBy')->shouldBeCalled()->willReturn([]);
127133
$queryBuilderProphecy->getDQLPart('join')->willReturn(['o' => []])->shouldBeCalled();
128134
$queryBuilderProphecy->innerJoin('o.author', 'author_a1', null, null)->shouldBeCalled();
129135
$queryBuilderProphecy->addOrderBy('author_a1.name', 'ASC')->shouldBeCalled();
@@ -148,6 +154,7 @@ public function testApplyToCollectionWithOrderOverriddenWithEmbeddedAssociation(
148154
{
149155
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
150156
$queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
157+
$queryBuilderProphecy->getDQLPart('orderBy')->shouldBeCalled()->willReturn([]);
151158
$queryBuilderProphecy->getRootAliases()->willReturn(['o']);
152159
$queryBuilderProphecy->addOrderBy('o.embeddedDummy.dummyName', 'DESC')->shouldBeCalled();
153160

@@ -166,4 +173,17 @@ public function testApplyToCollectionWithOrderOverriddenWithEmbeddedAssociation(
166173
$orderExtensionTest = new OrderExtension('asc', $resourceMetadataFactoryProphecy->reveal());
167174
$orderExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), EmbeddedDummy::class);
168175
}
176+
177+
public function testApplyToCollectionWithExistingOrderByDql()
178+
{
179+
$queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
180+
181+
$queryBuilderProphecy->getDQLPart('orderBy')->shouldBeCalled()->willReturn([new OrderBy('o.name')]);
182+
$queryBuilderProphecy->getEntityManager()->shouldNotBeCalled();
183+
$queryBuilderProphecy->getRootAliases()->shouldNotBeCalled();
184+
185+
$queryBuilder = $queryBuilderProphecy->reveal();
186+
$orderExtensionTest = new OrderExtension();
187+
$orderExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class);
188+
}
169189
}

0 commit comments

Comments
 (0)