Skip to content

Commit 9ff295c

Browse files
committed
Fix partial pagination which no longer returns the "hydra:next" property
1 parent 06b42a7 commit 9ff295c

File tree

3 files changed

+106
-45
lines changed

3 files changed

+106
-45
lines changed

features/hydra/collection.feature

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,13 @@ Feature: Collections support
203203
"hydra:next": {"pattern": "^/dummies\\?partial=1&page=8$"},
204204
"hydra:previous": {"pattern": "^/dummies\\?partial=1&page=6$"}
205205
},
206-
"additionalProperties": false
206+
"required": ["@id", "@type", "hydra:next", "hydra:previous"],
207+
"additionalProperties": false,
208+
"maxProperties": 4
207209
}
208-
}
210+
},
211+
"required": ["@context", "@id", "@type", "hydra:member", "hydra:view", "hydra:search"],
212+
"maxProperties": 6
209213
}
210214
"""
211215

@@ -275,16 +279,16 @@ Feature: Collections support
275279
And the response should be in JSON
276280
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
277281
And the JSON should be valid according to this schema:
278-
"""
279-
{
280-
"@id":"/dummies?page=3",
281-
"@type":"hydra:PartialCollectionView",
282-
"hydra:first":"/dummies?page=1",
283-
"hydra:last":"/dummies?page=10",
284-
"hydra:previous":"/dummies?page=2",
285-
"hydra:next":"/dummies?page=4"
286-
}
287-
"""
282+
"""
283+
{
284+
"@id":"/dummies?page=3",
285+
"@type":"hydra:PartialCollectionView",
286+
"hydra:first":"/dummies?page=1",
287+
"hydra:last":"/dummies?page=10",
288+
"hydra:previous":"/dummies?page=2",
289+
"hydra:next":"/dummies?page=4"
290+
}
291+
"""
288292
Scenario: Filter with exact match
289293
When I send a "GET" request to "/dummies?id=8"
290294
Then the response status code should be 200

src/Hydra/Serializer/PartialCollectionViewNormalizer.php

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public function normalize($object, $format = null, array $context = [])
6262
}
6363

6464
$currentPage = $lastPage = $itemsPerPage = $pageTotalItems = null;
65-
if ($paginated = $object instanceof PartialPaginatorInterface) {
65+
if ($paginated = ($object instanceof PartialPaginatorInterface)) {
6666
if ($object instanceof PaginatorInterface) {
6767
$paginated = 1. !== $lastPage = $object->getLastPage();
6868
} else {
@@ -81,41 +81,20 @@ public function normalize($object, $format = null, array $context = [])
8181
return $data;
8282
}
8383

84+
$cursorPaginationAttribute = null;
8485
$metadata = isset($context['resource_class']) && null !== $this->resourceMetadataFactory ? $this->resourceMetadataFactory->create($context['resource_class']) : null;
8586
$isPaginatedWithCursor = $paginated && null !== $metadata && null !== $cursorPaginationAttribute = $metadata->getCollectionOperationAttribute($context['collection_operation_name'] ?? $context['subresource_operation_name'], 'pagination_via_cursor', null, true);
8687

87-
$data['hydra:view'] = [
88-
'@id' => IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated && !$isPaginatedWithCursor ? $currentPage : null),
89-
'@type' => 'hydra:PartialCollectionView',
90-
];
88+
$data['hydra:view'] = ['@id' => null, '@type' => 'hydra:PartialCollectionView'];
9189

9290
if ($isPaginatedWithCursor) {
93-
$objects = iterator_to_array($object);
94-
$firstObject = current($objects);
95-
$lastObject = end($objects);
96-
97-
$data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters']);
98-
99-
if (false !== $lastObject && isset($cursorPaginationAttribute)) {
100-
$data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, 1, $lastObject)));
101-
}
102-
103-
if (false !== $firstObject && isset($cursorPaginationAttribute)) {
104-
$data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, -1, $firstObject)));
105-
}
106-
} elseif ($paginated) {
107-
if (null !== $lastPage) {
108-
$data['hydra:view']['hydra:first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1.);
109-
$data['hydra:view']['hydra:last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage);
110-
}
91+
return $this->populateDataWithCursorBasedPagination($data, $parsed, $object, $cursorPaginationAttribute);
92+
}
11193

112-
if (1. !== $currentPage) {
113-
$data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1.);
114-
}
94+
$data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null);
11595

116-
if (null !== $lastPage && $currentPage < $lastPage || null === $lastPage && $pageTotalItems >= $itemsPerPage) {
117-
$data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1.);
118-
}
96+
if ($paginated) {
97+
return $this->populateDataWithPagination($data, $parsed, $currentPage, $lastPage, $itemsPerPage, $pageTotalItems);
11998
}
12099

121100
return $data;
@@ -164,4 +143,41 @@ private function cursorPaginationFields(array $fields, int $direction, $object)
164143

165144
return $paginationFilters;
166145
}
146+
147+
private function populateDataWithCursorBasedPagination(array $data, array $parsed, \Traversable $object, $cursorPaginationAttribute): array
148+
{
149+
$objects = iterator_to_array($object);
150+
$firstObject = current($objects);
151+
$lastObject = end($objects);
152+
153+
$data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters']);
154+
155+
if (false !== $lastObject && \is_array($cursorPaginationAttribute)) {
156+
$data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, 1, $lastObject)));
157+
}
158+
159+
if (false !== $firstObject && \is_array($cursorPaginationAttribute)) {
160+
$data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, -1, $firstObject)));
161+
}
162+
163+
return $data;
164+
}
165+
166+
private function populateDataWithPagination(array $data, array $parsed, ?float $currentPage, ?float $lastPage, ?float $itemsPerPage, ?float $pageTotalItems): array
167+
{
168+
if (null !== $lastPage) {
169+
$data['hydra:view']['hydra:first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1.);
170+
$data['hydra:view']['hydra:last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage);
171+
}
172+
173+
if (1. !== $currentPage) {
174+
$data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1.);
175+
}
176+
177+
if ((null !== $lastPage && $currentPage < $lastPage) || (null === $lastPage && null === $lastPage && $pageTotalItems >= $itemsPerPage)) {
178+
$data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1.);
179+
}
180+
181+
return $data;
182+
}
167183
}

tests/Hydra/Serializer/PartialCollectionViewNormalizerTest.php

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use ApiPlatform\Core\DataProvider\PartialPaginatorInterface;
1818
use ApiPlatform\Core\Hydra\Serializer\PartialCollectionViewNormalizer;
1919
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
20+
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
21+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\SoMany;
2022
use ApiPlatform\Core\Tests\ProphecyTrait;
2123
use PHPUnit\Framework\TestCase;
2224
use Prophecy\Argument;
@@ -86,7 +88,24 @@ public function testNormalizePartialPaginator()
8688
);
8789
}
8890

89-
private function normalizePaginator($partial = false)
91+
public function testNormalizeCursorBasedPaginator(): void
92+
{
93+
self::assertEquals(
94+
[
95+
'foo' => 'bar',
96+
'hydra:totalItems' => 40,
97+
'hydra:view' => [
98+
'@id' => '/',
99+
'@type' => 'hydra:PartialCollectionView',
100+
'hydra:previous' => '/?id%5Bgt%5D=1',
101+
'hydra:next' => '/?id%5Blt%5D=2',
102+
],
103+
],
104+
$this->normalizePaginator(false, true)
105+
);
106+
}
107+
108+
private function normalizePaginator(bool $partial = false, bool $cursor = false)
90109
{
91110
$paginatorProphecy = $this->prophesize($partial ? PartialPaginatorInterface::class : PaginatorInterface::class);
92111
$paginatorProphecy->getCurrentPage()->willReturn(3)->shouldBeCalled();
@@ -103,11 +122,33 @@ private function normalizePaginator($partial = false)
103122

104123
$decoratedNormalizerProphecy = $this->prophesize(NormalizerInterface::class);
105124
$decoratedNormalizerProphecy->normalize(Argument::type($partial ? PartialPaginatorInterface::class : PaginatorInterface::class), null, Argument::type('array'))->willReturn($decoratedNormalize)->shouldBeCalled();
106-
$resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class);
107125

108-
$normalizer = new PartialCollectionViewNormalizer($decoratedNormalizerProphecy->reveal(), '_page', 'pagination', $resourceMetadataFactory->reveal());
126+
$resourceMetadataFactoryProphecy = null;
127+
128+
if ($cursor) {
129+
$firstSoMany = new SoMany();
130+
$firstSoMany->id = 1;
131+
$firstSoMany->content = 'SoMany #1';
132+
133+
$lastSoMany = new SoMany();
134+
$lastSoMany->id = 2;
135+
$lastSoMany->content = 'SoMany #2';
136+
137+
$paginatorProphecy->rewind()->willReturn()->shouldBeCalledOnce();
138+
$paginatorProphecy->valid()->willReturn(true, true, false)->shouldBeCalledTimes(3);
139+
$paginatorProphecy->key()->willReturn(1, 2)->shouldBeCalledTimes(2);
140+
$paginatorProphecy->current()->willReturn($firstSoMany, $lastSoMany)->shouldBeCalledTimes(2);
141+
$paginatorProphecy->next()->willReturn()->shouldBeCalledTimes(2);
142+
143+
$soManyMetadata = new ResourceMetadata(null, null, null, null, ['get' => ['pagination_via_cursor' => [['field' => 'id', 'direction' => 'desc']]]]);
144+
145+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
146+
$resourceMetadataFactoryProphecy->create(SoMany::class)->willReturn($soManyMetadata)->shouldBeCalledOnce();
147+
}
148+
149+
$normalizer = new PartialCollectionViewNormalizer($decoratedNormalizerProphecy->reveal(), '_page', 'pagination', $resourceMetadataFactoryProphecy ? $resourceMetadataFactoryProphecy->reveal() : null);
109150

110-
return $normalizer->normalize($paginatorProphecy->reveal());
151+
return $normalizer->normalize($paginatorProphecy->reveal(), null, ['resource_class' => SoMany::class, 'collection_operation_name' => 'get']);
111152
}
112153

113154
public function testSupportsNormalization()

0 commit comments

Comments
 (0)