Skip to content

Commit 6948b5e

Browse files
committed
feat: add CursorPaginator
1 parent 5e908c8 commit 6948b5e

File tree

5 files changed

+174
-2
lines changed

5 files changed

+174
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Hydra\Serializer;
15+
16+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
17+
use ApiPlatform\Metadata\UrlGeneratorInterface;
18+
use ApiPlatform\Serializer\CacheableSupportsMethodInterface;
19+
use ApiPlatform\State\Pagination\CursorPaginatorInterface;
20+
use ApiPlatform\Util\IriHelper;
21+
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
22+
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface;
23+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
24+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
25+
use Symfony\Component\Serializer\Serializer;
26+
27+
/**
28+
* Adds a view key to the result of a paginated Hydra collection, if the
29+
* collection is a CursorPaginatorInterface.
30+
*
31+
* @author Priyadi Iman Nurcahyo <[email protected]>
32+
*/
33+
final class CursorBasedPartialCollectionViewNormalizer implements NormalizerInterface, NormalizerAwareInterface, CacheableSupportsMethodInterface
34+
{
35+
public function __construct(private readonly NormalizerInterface $collectionNormalizer, private readonly string $pageParameterName = 'page', private readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private readonly int $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH)
36+
{
37+
}
38+
39+
/**
40+
* {@inheritdoc}
41+
*/
42+
public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
43+
{
44+
$data = $this->collectionNormalizer->normalize($object, $format, $context);
45+
46+
if (!$object instanceof CursorPaginatorInterface || isset($context['api_sub_level'])) {
47+
return $data;
48+
}
49+
50+
if (!\is_array($data)) {
51+
throw new UnexpectedValueException('Expected data to be an array');
52+
}
53+
54+
// (same TODO message retained from PartialCollectionViewNormalizer)
55+
// TODO: This needs to be changed as well as I wrote in the CollectionFiltersNormalizer
56+
// We should not rely on the request_uri but instead rely on the UriTemplate
57+
// This needs that we implement the RFC and that we do more parsing before calling the serialization (MainController)
58+
$parsed = IriHelper::parseIri($context['uri'] ?? $context['request_uri'] ?? '/', $this->pageParameterName);
59+
60+
$operation = $context['operation'] ?? null;
61+
if (!$operation && $this->resourceMetadataFactory && isset($context['resource_class'])) {
62+
$operation = $this->resourceMetadataFactory->create($context['resource_class'])->getOperation($context['operation_name'] ?? null);
63+
}
64+
65+
$data['hydra:view'] = ['@type' => 'hydra:PartialCollectionView'];
66+
67+
$data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $object->getCurrentPageCursor(), $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
68+
69+
if (($firstPageCursor = $object->getFirstPageCursor()) !== null) {
70+
$data['hydra:view']['hydra:first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $firstPageCursor, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
71+
}
72+
73+
if (($lastPageCursor = $object->getLastPageCursor()) !== null) {
74+
$data['hydra:view']['hydra:last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPageCursor, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
75+
}
76+
77+
if (($nextPageCursor = $object->getNextPageCursor()) !== null) {
78+
$data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $nextPageCursor, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
79+
}
80+
81+
if (($previousPageCursor = $object->getPreviousPageCursor()) !== null) {
82+
$data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $previousPageCursor, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
83+
}
84+
85+
return $data;
86+
}
87+
88+
/**
89+
* {@inheritdoc}
90+
*/
91+
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
92+
{
93+
return $this->collectionNormalizer->supportsNormalization($data, $format, $context);
94+
}
95+
96+
public function getSupportedTypes($format): array
97+
{
98+
// @deprecated remove condition when support for symfony versions under 6.3 is dropped
99+
if (!method_exists($this->collectionNormalizer, 'getSupportedTypes')) {
100+
return [
101+
'*' => $this->collectionNormalizer instanceof CacheableSupportsMethodInterface && $this->collectionNormalizer->hasCacheableSupportsMethod(),
102+
];
103+
}
104+
105+
return $this->collectionNormalizer->getSupportedTypes($format);
106+
}
107+
108+
public function hasCacheableSupportsMethod(): bool
109+
{
110+
if (method_exists(Serializer::class, 'getSupportedTypes')) {
111+
trigger_deprecation(
112+
'api-platform/core',
113+
'3.1',
114+
'The "%s()" method is deprecated, use "getSupportedTypes()" instead.',
115+
__METHOD__
116+
);
117+
}
118+
119+
return $this->collectionNormalizer instanceof BaseCacheableSupportsMethodInterface && $this->collectionNormalizer->hasCacheableSupportsMethod();
120+
}
121+
122+
/**
123+
* {@inheritdoc}
124+
*/
125+
public function setNormalizer(NormalizerInterface $normalizer): void
126+
{
127+
if ($this->collectionNormalizer instanceof NormalizerAwareInterface) {
128+
$this->collectionNormalizer->setNormalizer($normalizer);
129+
}
130+
}
131+
}

src/OpenApi/Factory/OpenApiFactory.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,7 @@ private function getPaginationParameters(CollectionOperationInterface|HttpOperat
663663
$parameters = [];
664664

665665
if ($operation->getPaginationEnabled() ?? $this->paginationOptions->isPaginationEnabled()) {
666-
$parameters[] = new Parameter($this->paginationOptions->getPaginationPageParameterName(), 'query', 'The collection page number', false, false, true, ['type' => 'integer', 'default' => 1]);
666+
$parameters[] = new Parameter($this->paginationOptions->getPaginationPageParameterName(), 'query', 'The collection page identifier', false, false, true, ['type' => 'string']);
667667

668668
if ($operation->getPaginationClientItemsPerPage() ?? $this->paginationOptions->getClientItemsPerPage()) {
669669
$schema = [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\State\Pagination;
15+
16+
/**
17+
* @author Priyadi Iman Nurcahyo <[email protected]>
18+
*
19+
* @template T of object
20+
*
21+
* @extends \Traversable<T>
22+
*/
23+
interface CursorPaginatorInterface extends \Countable, \Traversable
24+
{
25+
public function getCurrentPageCursor(): ?string;
26+
27+
public function getNextPageCursor(): ?string;
28+
29+
public function getPreviousPageCursor(): ?string;
30+
31+
public function getFirstPageCursor(): ?string;
32+
33+
public function getLastPageCursor(): ?string;
34+
}

src/Symfony/Bundle/Resources/config/hydra.xml

+7
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@
6464
<argument>%api_platform.url_generation_strategy%</argument>
6565
</service>
6666

67+
<service id="api_platform.hydra.normalizer.cursor_based_partial_collection_view" class="ApiPlatform\Hydra\Serializer\CursorBasedPartialCollectionViewNormalizer" decorates="api_platform.hydra.normalizer.collection" public="false">
68+
<argument type="service" id="api_platform.hydra.normalizer.cursor_based_partial_collection_view.inner" />
69+
<argument>%api_platform.collection.pagination.page_parameter_name%</argument>
70+
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
71+
<argument>%api_platform.url_generation_strategy%</argument>
72+
</service>
73+
6774
<service id="api_platform.hydra.normalizer.collection_filters" class="ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer" decorates="api_platform.hydra.normalizer.collection" public="false">
6875
<argument type="service" id="api_platform.hydra.normalizer.collection_filters.inner" />
6976
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />

src/Util/IriHelper.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public static function parseIri(string $iri, string $pageParameterName): array
5656
/**
5757
* Gets a collection IRI for the given parameters.
5858
*/
59-
public static function createIri(array $parts, array $parameters, ?string $pageParameterName = null, ?float $page = null, $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH): string
59+
public static function createIri(array $parts, array $parameters, ?string $pageParameterName = null, null|float|string $page = null, $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH): string
6060
{
6161
if (null !== $page && null !== $pageParameterName) {
6262
$parameters[$pageParameterName] = $page;

0 commit comments

Comments
 (0)