Skip to content

Commit

Permalink
Fix partial pagination which no longer returns the "hydra:next" prope…
Browse files Browse the repository at this point in the history
…rty (api-platform#4015)

* Revert "fix: only display hydra:next when the item total is strictly greater than the number of items per page (api-platform#3967)"

This reverts commit 9cecfab.

* Fix partial pagination which no longer returns the "hydra:next" property
  • Loading branch information
meyerbaptiste committed Feb 8, 2021
1 parent 2edb3bf commit a5196ae
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 45 deletions.
28 changes: 16 additions & 12 deletions features/hydra/collection.feature
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,13 @@ Feature: Collections support
"hydra:next": {"pattern": "^/dummies\\?partial=1&page=8$"},
"hydra:previous": {"pattern": "^/dummies\\?partial=1&page=6$"}
},
"additionalProperties": false
"required": ["@id", "@type", "hydra:next", "hydra:previous"],
"additionalProperties": false,
"maxProperties": 4
}
}
},
"required": ["@context", "@id", "@type", "hydra:member", "hydra:view", "hydra:search"],
"maxProperties": 6
}
"""

Expand Down Expand Up @@ -275,16 +279,16 @@ Feature: Collections support
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be valid according to this schema:
"""
{
"@id":"/dummies?page=3",
"@type":"hydra:PartialCollectionView",
"hydra:first":"/dummies?page=1",
"hydra:last":"/dummies?page=10",
"hydra:previous":"/dummies?page=2",
"hydra:next":"/dummies?page=4"
}
"""
"""
{
"@id":"/dummies?page=3",
"@type":"hydra:PartialCollectionView",
"hydra:first":"/dummies?page=1",
"hydra:last":"/dummies?page=10",
"hydra:previous":"/dummies?page=2",
"hydra:next":"/dummies?page=4"
}
"""
Scenario: Filter with exact match
When I send a "GET" request to "/dummies?id=8"
Then the response status code should be 200
Expand Down
74 changes: 45 additions & 29 deletions src/Hydra/Serializer/PartialCollectionViewNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public function normalize($object, $format = null, array $context = [])
}

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

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

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

if ($isPaginatedWithCursor) {
$objects = iterator_to_array($object);
$firstObject = current($objects);
$lastObject = end($objects);

$data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters']);

if (false !== $lastObject && isset($cursorPaginationAttribute)) {
$data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, 1, $lastObject)));
}

if (false !== $firstObject && isset($cursorPaginationAttribute)) {
$data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, -1, $firstObject)));
}
} elseif ($paginated) {
if (null !== $lastPage) {
$data['hydra:view']['hydra:first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1.);
$data['hydra:view']['hydra:last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage);
}
return $this->populateDataWithCursorBasedPagination($data, $parsed, $object, $cursorPaginationAttribute);
}

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

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

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

return $paginationFilters;
}

private function populateDataWithCursorBasedPagination(array $data, array $parsed, \Traversable $object, $cursorPaginationAttribute): array
{
$objects = iterator_to_array($object);
$firstObject = current($objects);
$lastObject = end($objects);

$data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters']);

if (false !== $lastObject && \is_array($cursorPaginationAttribute)) {
$data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, 1, $lastObject)));
}

if (false !== $firstObject && \is_array($cursorPaginationAttribute)) {
$data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, -1, $firstObject)));
}

return $data;
}

private function populateDataWithPagination(array $data, array $parsed, ?float $currentPage, ?float $lastPage, ?float $itemsPerPage, ?float $pageTotalItems): array
{
if (null !== $lastPage) {
$data['hydra:view']['hydra:first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1.);
$data['hydra:view']['hydra:last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage);
}

if (1. !== $currentPage) {
$data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1.);
}

if ((null !== $lastPage && $currentPage < $lastPage) || (null === $lastPage && $pageTotalItems >= $itemsPerPage)) {
$data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1.);
}

return $data;
}
}
50 changes: 46 additions & 4 deletions tests/Hydra/Serializer/PartialCollectionViewNormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use ApiPlatform\Core\DataProvider\PartialPaginatorInterface;
use ApiPlatform\Core\Hydra\Serializer\PartialCollectionViewNormalizer;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\SoMany;
use ApiPlatform\Core\Tests\ProphecyTrait;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
Expand Down Expand Up @@ -79,13 +81,31 @@ public function testNormalizePartialPaginator()
'@id' => '/?_page=3',
'@type' => 'hydra:PartialCollectionView',
'hydra:previous' => '/?_page=2',
'hydra:next' => '/?_page=4',
],
],
$this->normalizePaginator(true)
);
}

private function normalizePaginator($partial = false)
public function testNormalizeWithCursorBasedPagination(): void
{
self::assertEquals(
[
'foo' => 'bar',
'hydra:totalItems' => 40,
'hydra:view' => [
'@id' => '/',
'@type' => 'hydra:PartialCollectionView',
'hydra:previous' => '/?id%5Bgt%5D=1',
'hydra:next' => '/?id%5Blt%5D=2',
],
],
$this->normalizePaginator(false, true)
);
}

private function normalizePaginator(bool $partial = false, bool $cursor = false)
{
$paginatorProphecy = $this->prophesize($partial ? PartialPaginatorInterface::class : PaginatorInterface::class);
$paginatorProphecy->getCurrentPage()->willReturn(3)->shouldBeCalled();
Expand All @@ -102,11 +122,33 @@ private function normalizePaginator($partial = false)

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

$normalizer = new PartialCollectionViewNormalizer($decoratedNormalizerProphecy->reveal(), '_page', 'pagination', $resourceMetadataFactory->reveal());
$resourceMetadataFactoryProphecy = null;

if ($cursor) {
$firstSoMany = new SoMany();
$firstSoMany->id = 1;
$firstSoMany->content = 'SoMany #1';

$lastSoMany = new SoMany();
$lastSoMany->id = 2;
$lastSoMany->content = 'SoMany #2';

$paginatorProphecy->rewind()->willReturn()->shouldBeCalledOnce();
$paginatorProphecy->valid()->willReturn(true, true, false)->shouldBeCalledTimes(3);
$paginatorProphecy->key()->willReturn(1, 2)->shouldBeCalledTimes(2);
$paginatorProphecy->current()->willReturn($firstSoMany, $lastSoMany)->shouldBeCalledTimes(2);
$paginatorProphecy->next()->willReturn()->shouldBeCalledTimes(2);

$soManyMetadata = new ResourceMetadata(null, null, null, null, ['get' => ['pagination_via_cursor' => [['field' => 'id', 'direction' => 'desc']]]]);

$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
$resourceMetadataFactoryProphecy->create(SoMany::class)->willReturn($soManyMetadata)->shouldBeCalledOnce();
}

$normalizer = new PartialCollectionViewNormalizer($decoratedNormalizerProphecy->reveal(), '_page', 'pagination', $resourceMetadataFactoryProphecy ? $resourceMetadataFactoryProphecy->reveal() : null);

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

public function testSupportsNormalization()
Expand Down

0 comments on commit a5196ae

Please sign in to comment.