Skip to content

Commit

Permalink
feat(paginator): add CursorPaginator
Browse files Browse the repository at this point in the history
  • Loading branch information
priyadi committed Apr 11, 2024
1 parent 5e908c8 commit 63d7727
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 2 deletions.
131 changes: 131 additions & 0 deletions src/Hydra/Serializer/CursorBasedPartialCollectionViewNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Hydra\Serializer;

use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use ApiPlatform\Serializer\CacheableSupportsMethodInterface;
use ApiPlatform\State\Pagination\CursorPaginatorInterface;
use ApiPlatform\Util\IriHelper;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Serializer;

/**
* Adds a view key to the result of a paginated Hydra collection, if the
* collection is a CursorPaginatorInterface.
*
* @author Priyadi Iman Nurcahyo <[email protected]>
*/
final class CursorBasedPartialCollectionViewNormalizer implements NormalizerInterface, NormalizerAwareInterface, CacheableSupportsMethodInterface
{
public function __construct(private readonly NormalizerInterface $collectionNormalizer, private readonly string $pageParameterName = 'page', private readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private readonly int $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH)
{
}

/**
* {@inheritdoc}
*/
public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
$data = $this->collectionNormalizer->normalize($object, $format, $context);

if (!$object instanceof CursorPaginatorInterface || isset($context['api_sub_level'])) {
return $data;
}

if (!\is_array($data)) {
throw new UnexpectedValueException('Expected data to be an array');
}

// (same TODO message retained from PartialCollectionViewNormalizer)
// TODO: This needs to be changed as well as I wrote in the CollectionFiltersNormalizer
// We should not rely on the request_uri but instead rely on the UriTemplate
// This needs that we implement the RFC and that we do more parsing before calling the serialization (MainController)
$parsed = IriHelper::parseIri($context['uri'] ?? $context['request_uri'] ?? '/', $this->pageParameterName);

$operation = $context['operation'] ?? null;
if (!$operation && $this->resourceMetadataFactory && isset($context['resource_class'])) {
$operation = $this->resourceMetadataFactory->create($context['resource_class'])->getOperation($context['operation_name'] ?? null);
}

$data['hydra:view'] = ['@type' => 'hydra:PartialCollectionView'];

$data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $object->getCurrentPageCursor(), $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);

if (($firstPageCursor = $object->getFirstPageCursor()) !== null) {
$data['hydra:view']['hydra:first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $firstPageCursor, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
}

if (($lastPageCursor = $object->getLastPageCursor()) !== null) {
$data['hydra:view']['hydra:last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPageCursor, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
}

if (($nextPageCursor = $object->getNextPageCursor()) !== null) {
$data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $nextPageCursor, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
}

if (($previousPageCursor = $object->getPreviousPageCursor()) !== null) {
$data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $previousPageCursor, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
}

return $data;
}

/**
* {@inheritdoc}
*/
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $this->collectionNormalizer->supportsNormalization($data, $format, $context);
}

public function getSupportedTypes($format): array
{
// @deprecated remove condition when support for symfony versions under 6.3 is dropped
if (!method_exists($this->collectionNormalizer, 'getSupportedTypes')) {
return [
'*' => $this->collectionNormalizer instanceof CacheableSupportsMethodInterface && $this->collectionNormalizer->hasCacheableSupportsMethod(),

Check failure on line 101 in src/Hydra/Serializer/CursorBasedPartialCollectionViewNormalizer.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.3)

Call to an undefined method ApiPlatform\Serializer\CacheableSupportsMethodInterface&Symfony\Component\Serializer\Normalizer\NormalizerInterface::hasCacheableSupportsMethod().
];
}

return $this->collectionNormalizer->getSupportedTypes($format);
}

public function hasCacheableSupportsMethod(): bool
{
if (method_exists(Serializer::class, 'getSupportedTypes')) {
trigger_deprecation(
'api-platform/core',
'3.1',
'The "%s()" method is deprecated, use "getSupportedTypes()" instead.',
__METHOD__
);
}

return $this->collectionNormalizer instanceof BaseCacheableSupportsMethodInterface && $this->collectionNormalizer->hasCacheableSupportsMethod();
}

/**
* {@inheritdoc}
*/
public function setNormalizer(NormalizerInterface $normalizer): void
{
if ($this->collectionNormalizer instanceof NormalizerAwareInterface) {
$this->collectionNormalizer->setNormalizer($normalizer);
}
}
}
2 changes: 1 addition & 1 deletion src/OpenApi/Factory/OpenApiFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ private function getPaginationParameters(CollectionOperationInterface|HttpOperat
$parameters = [];

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

if ($operation->getPaginationClientItemsPerPage() ?? $this->paginationOptions->getClientItemsPerPage()) {
$schema = [
Expand Down
34 changes: 34 additions & 0 deletions src/State/Pagination/CursorPaginatorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\State\Pagination;

/**
* @author Priyadi Iman Nurcahyo <[email protected]>
*
* @template T of object
*
* @extends \Traversable<T>
*/
interface CursorPaginatorInterface extends \Countable, \Traversable
{
public function getCurrentPageCursor(): ?string;

public function getNextPageCursor(): ?string;

public function getPreviousPageCursor(): ?string;

public function getFirstPageCursor(): ?string;

public function getLastPageCursor(): ?string;
}
7 changes: 7 additions & 0 deletions src/Symfony/Bundle/Resources/config/hydra.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@
<argument>%api_platform.url_generation_strategy%</argument>
</service>

<service id="api_platform.hydra.normalizer.cursor_based_partial_collection_view" class="ApiPlatform\Hydra\Serializer\CursorBasedPartialCollectionViewNormalizer" decorates="api_platform.hydra.normalizer.collection" public="false">
<argument type="service" id="api_platform.hydra.normalizer.cursor_based_partial_collection_view.inner" />
<argument>%api_platform.collection.pagination.page_parameter_name%</argument>
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
<argument>%api_platform.url_generation_strategy%</argument>
</service>

<service id="api_platform.hydra.normalizer.collection_filters" class="ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer" decorates="api_platform.hydra.normalizer.collection" public="false">
<argument type="service" id="api_platform.hydra.normalizer.collection_filters.inner" />
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
Expand Down
2 changes: 1 addition & 1 deletion src/Util/IriHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public static function parseIri(string $iri, string $pageParameterName): array
/**
* Gets a collection IRI for the given parameters.
*/
public static function createIri(array $parts, array $parameters, ?string $pageParameterName = null, ?float $page = null, $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH): string
public static function createIri(array $parts, array $parameters, ?string $pageParameterName = null, null|float|string $page = null, $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH): string
{
if (null !== $page && null !== $pageParameterName) {
$parameters[$pageParameterName] = $page;
Expand Down

0 comments on commit 63d7727

Please sign in to comment.