Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
use ApiPlatform\State\Processor\AddLinkHeaderProcessor;
use ApiPlatform\State\Processor\ObjectMapperInputProcessor;
use ApiPlatform\State\Processor\ObjectMapperOutputProcessor;
use ApiPlatform\State\Processor\PaginationLinkProcessor;
use ApiPlatform\State\Processor\RespondProcessor;
use ApiPlatform\State\Processor\SerializeProcessor;
use ApiPlatform\State\Processor\WriteProcessor;
Expand Down Expand Up @@ -552,7 +553,7 @@ public function register(): void
);
}

return new AddLinkHeaderProcessor($decorated, new HttpHeaderSerializer());
return new AddLinkHeaderProcessor(new PaginationLinkProcessor($decorated, $app->make(Pagination::class)), new HttpHeaderSerializer());
});

$this->app->singleton(SerializeProcessor::class, static function (Application $app) {
Expand Down
14 changes: 14 additions & 0 deletions src/Metadata/ApiResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,7 @@ public function __construct(
protected array $extraProperties = [],
?bool $map = null,
protected ?array $mcp = null,
protected ?bool $paginationLinkHeader = null,
) {
parent::__construct(
shortName: $shortName,
Expand Down Expand Up @@ -1273,6 +1274,19 @@ public function withAcceptPatch(string $acceptPatch): static
return $self;
}

public function getPaginationLinkHeader(): ?bool
{
return $this->paginationLinkHeader;
}

public function withPaginationLinkHeader(bool $paginationLinkHeader): static
{
$self = clone $this;
$self->paginationLinkHeader = $paginationLinkHeader;

return $self;
}

public function getStatus(): ?int
{
return $this->status;
Expand Down
2 changes: 2 additions & 0 deletions src/Metadata/Extractor/XmlResourceExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ private function buildBase(\SimpleXMLElement $resource): array
'paginationClientItemsPerPage' => $this->phpize($resource, 'paginationClientItemsPerPage', 'bool'),
'paginationClientPartial' => $this->phpize($resource, 'paginationClientPartial', 'bool'),
'paginationEnabled' => $this->phpize($resource, 'paginationEnabled', 'bool'),
'paginationLinkHeader' => $this->phpize($resource, 'paginationLinkHeader', 'bool'),
'paginationFetchJoinCollection' => $this->phpize($resource, 'paginationFetchJoinCollection', 'bool'),
'paginationUseOutputWalkers' => $this->phpize($resource, 'paginationUseOutputWalkers', 'bool'),
'paginationItemsPerPage' => $this->phpize($resource, 'paginationItemsPerPage', 'integer'),
Expand Down Expand Up @@ -429,6 +430,7 @@ private function buildOperations(\SimpleXMLElement $resource, array $root): ?arr
'queryParameterValidate' => $this->phpize($operation, 'queryParameterValidate', 'bool'),
'priority' => $this->phpize($operation, 'priority', 'integer'),
'routePriority' => $this->phpize($operation, 'routePriority', 'integer'),
'paginationLinkHeader' => $this->phpize($operation, 'paginationLinkHeader', 'bool'),
'name' => $this->phpize($operation, 'name', 'string'),
'routeName' => $this->phpize($operation, 'routeName', 'string'),
]);
Expand Down
2 changes: 2 additions & 0 deletions src/Metadata/Extractor/YamlResourceExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ private function buildBase(array $resource): array
'paginationClientItemsPerPage' => $this->phpize($resource, 'paginationClientItemsPerPage', 'bool'),
'paginationClientPartial' => $this->phpize($resource, 'paginationClientPartial', 'bool'),
'paginationEnabled' => $this->phpize($resource, 'paginationEnabled', 'bool'),
'paginationLinkHeader' => $this->phpize($resource, 'paginationLinkHeader', 'bool'),
'paginationFetchJoinCollection' => $this->phpize($resource, 'paginationFetchJoinCollection', 'bool'),
'paginationUseOutputWalkers' => $this->phpize($resource, 'paginationUseOutputWalkers', 'bool'),
'paginationItemsPerPage' => $this->phpize($resource, 'paginationItemsPerPage', 'integer'),
Expand Down Expand Up @@ -366,6 +367,7 @@ private function buildOperations(array $resource, array $root): ?array
'hideHydraOperation' => $this->phpize($resource, 'hideHydraOperation', 'bool'),
'priority' => $this->phpize($operation, 'priority', 'integer'),
'routePriority' => $this->phpize($operation, 'routePriority', 'integer'),
'paginationLinkHeader' => $this->phpize($operation, 'paginationLinkHeader', 'bool'),
'name' => $this->phpize($operation, 'name', 'string'),
'class' => (string) $class,
]);
Expand Down
1 change: 1 addition & 0 deletions src/Metadata/Extractor/schema/resources.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@
<xsd:attribute type="xsd:boolean" name="stateless"/>
<xsd:attribute type="xsd:string" name="sunset"/>
<xsd:attribute type="xsd:string" name="acceptPatch"/>
<xsd:attribute type="xsd:boolean" name="paginationLinkHeader"/>
<xsd:attribute type="xsd:positiveInteger" name="status"/>
<xsd:attribute type="xsd:string" name="host"/>
<xsd:attribute type="xsd:string" name="condition"/>
Expand Down
4 changes: 3 additions & 1 deletion src/Metadata/GetCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public function __construct(
?bool $throwOnNotFound = null,
private ?string $itemUriTemplate = null,
?bool $map = null,
?bool $paginationLinkHeader = null,
) {
parent::__construct(
uriTemplate: $uriTemplate,
Expand Down Expand Up @@ -192,7 +193,8 @@ class: $class,
strictQueryParameterValidation: $strictQueryParameterValidation,
hideHydraOperation: $hideHydraOperation,
stateOptions: $stateOptions,
map: $map
map: $map,
paginationLinkHeader: $paginationLinkHeader
);
}

Expand Down
14 changes: 14 additions & 0 deletions src/Metadata/HttpOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ public function __construct(
?bool $throwOnNotFound = null,
array $extraProperties = [],
?bool $map = null,
protected ?bool $paginationLinkHeader = null,
) {
$this->formats = (null === $formats || \is_array($formats)) ? $formats : [$formats];
$this->inputFormats = (null === $inputFormats || \is_array($inputFormats)) ? $inputFormats : [$inputFormats];
Expand Down Expand Up @@ -518,6 +519,19 @@ public function withAcceptPatch(string $acceptPatch): static
return $self;
}

public function getPaginationLinkHeader(): ?bool
{
return $this->paginationLinkHeader;
}

public function withPaginationLinkHeader(bool $paginationLinkHeader): static
{
$self = clone $this;
$self->paginationLinkHeader = $paginationLinkHeader;

return $self;
}

public function getStatus(): ?int
{
return $this->status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ final class XmlResourceAdapter implements ResourceAdapterInterface
'paginationClientItemsPerPage',
'paginationClientPartial',
'paginationEnabled',
'paginationLinkHeader',
'paginationFetchJoinCollection',
'paginationUseOutputWalkers',
'paginationItemsPerPage',
Expand Down
2 changes: 1 addition & 1 deletion src/Metadata/Tests/Extractor/Adapter/resources.xml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/Metadata/Tests/Extractor/Adapter/resources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ resources:
paginationViaCursor:
userId: DESC
paginationEnabled: false
paginationLinkHeader: false
paginationFetchJoinCollection: false
paginationUseOutputWalkers: false
paginationItemsPerPage: 54
Expand Down Expand Up @@ -350,3 +351,4 @@ resources:
'Lorem ipsum': 'Dolor sit amet'
map: null
mcp: null
paginationLinkHeader: true
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase
'paginationClientItemsPerPage' => true,
'paginationClientPartial' => true,
'paginationEnabled' => true,
'paginationLinkHeader' => true,
'paginationFetchJoinCollection' => true,
'paginationUseOutputWalkers' => true,
'paginationItemsPerPage' => 42,
Expand Down Expand Up @@ -389,6 +390,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase
'userId' => 'DESC',
],
'paginationEnabled' => false,
'paginationLinkHeader' => false,
'paginationFetchJoinCollection' => false,
'paginationUseOutputWalkers' => false,
'paginationItemsPerPage' => 54,
Expand Down Expand Up @@ -513,6 +515,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase
'jsonldContext',
'openapi',
'paginationViaCursor',
'paginationLinkHeader',
'stateOptions',
'links',
'rules',
Expand Down
4 changes: 4 additions & 0 deletions src/Metadata/Tests/Extractor/XmlExtractorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public function testValidXML(): void
'paginationClientItemsPerPage' => null,
'paginationClientPartial' => null,
'paginationEnabled' => null,
'paginationLinkHeader' => null,
'paginationFetchJoinCollection' => null,
'paginationUseOutputWalkers' => null,
'paginationItemsPerPage' => null,
Expand Down Expand Up @@ -132,6 +133,7 @@ public function testValidXML(): void
'paginationClientItemsPerPage' => null,
'paginationClientPartial' => null,
'paginationEnabled' => null,
'paginationLinkHeader' => null,
'paginationFetchJoinCollection' => null,
'paginationUseOutputWalkers' => null,
'paginationItemsPerPage' => null,
Expand Down Expand Up @@ -280,6 +282,7 @@ public function testValidXML(): void
'method' => null,
'priority' => null,
'routePriority' => null,
'paginationLinkHeader' => null,
'processor' => null,
'provider' => null,
'itemUriTemplate' => null,
Expand Down Expand Up @@ -390,6 +393,7 @@ public function testValidXML(): void
'method' => null,
'priority' => null,
'routePriority' => null,
'paginationLinkHeader' => null,
'processor' => null,
'provider' => null,
'stateOptions' => null,
Expand Down
6 changes: 6 additions & 0 deletions src/Metadata/Tests/Extractor/YamlExtractorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public function testValidYaml(): void
'paginationClientItemsPerPage' => null,
'paginationClientPartial' => null,
'paginationEnabled' => null,
'paginationLinkHeader' => null,
'paginationFetchJoinCollection' => null,
'paginationUseOutputWalkers' => null,
'paginationItemsPerPage' => null,
Expand Down Expand Up @@ -131,6 +132,7 @@ public function testValidYaml(): void
'paginationClientItemsPerPage' => null,
'paginationClientPartial' => null,
'paginationEnabled' => null,
'paginationLinkHeader' => null,
'paginationFetchJoinCollection' => null,
'paginationUseOutputWalkers' => null,
'paginationItemsPerPage' => null,
Expand Down Expand Up @@ -205,6 +207,7 @@ public function testValidYaml(): void
'paginationClientItemsPerPage' => null,
'paginationClientPartial' => null,
'paginationEnabled' => null,
'paginationLinkHeader' => null,
'paginationFetchJoinCollection' => null,
'paginationUseOutputWalkers' => null,
'paginationItemsPerPage' => null,
Expand Down Expand Up @@ -321,6 +324,7 @@ public function testValidYaml(): void
'queryParameterValidate' => null,
'priority' => null,
'routePriority' => null,
'paginationLinkHeader' => null,
'processor' => null,
'provider' => null,
'itemUriTemplate' => null,
Expand Down Expand Up @@ -413,6 +417,7 @@ public function testValidYaml(): void
'queryParameterValidate' => null,
'priority' => null,
'routePriority' => null,
'paginationLinkHeader' => null,
'processor' => null,
'provider' => null,
'stateOptions' => null,
Expand Down Expand Up @@ -462,6 +467,7 @@ public function testValidYaml(): void
'paginationClientItemsPerPage' => null,
'paginationClientPartial' => null,
'paginationEnabled' => null,
'paginationLinkHeader' => null,
'paginationFetchJoinCollection' => null,
'paginationUseOutputWalkers' => null,
'paginationItemsPerPage' => null,
Expand Down
108 changes: 108 additions & 0 deletions src/State/Processor/PaginationLinkProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\Processor;

use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use ApiPlatform\Metadata\Util\IriHelper;
use ApiPlatform\State\Pagination\Pagination;
use ApiPlatform\State\Pagination\PaginatorInterface;
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\Util\CorsTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\WebLink\GenericLinkProvider;
use Symfony\Component\WebLink\Link;

/**
* A single-page collection still gets `first` and `last`, where the body links emit nothing: a HEAD response
* (RFC 9110 §9.3.2) carries no body from which a client could derive them. Cursor pagination is out of
* scope: RFC 8288 relations address pages, not cursors.
*
* @template T1
* @template T2
*
* @implements ProcessorInterface<T1, T2>
*
* @author Julien Robic <nayte91@gmail.com>
*/
final class PaginationLinkProcessor implements ProcessorInterface
{
use CorsTrait;

/**
* @param ProcessorInterface<T1, T2> $decorated
*/
public function __construct(private readonly ProcessorInterface $decorated, private readonly Pagination $pagination)
{
}

public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
{
if (
!($request = $context['request'] ?? null)
|| !$request instanceof Request
|| !$operation instanceof HttpOperation
|| !$operation instanceof CollectionOperationInterface
|| true !== $operation->getPaginationLinkHeader()
|| null !== $operation->getPaginationViaCursor()
|| $this->isPreflightRequest($request)
) {
return $this->decorated->process($data, $operation, $uriVariables, $context);
}

$paginator = $context['original_data'] ?? $data;
if (!$paginator instanceof PartialPaginatorInterface) {
return $this->decorated->process($data, $operation, $uriVariables, $context);
}

$options = $this->pagination->getOptions();
$pageParameterName = $options['page_parameter_name'];
$parsed = IriHelper::parseIri($request->getUri(), $pageParameterName);
$parameters = $parsed['parameters'];

$itemsPerPage = $paginator->getItemsPerPage();

$urlGenerationStrategy = $operation->getUrlGenerationStrategy() ?? UrlGeneratorInterface::ABS_PATH;
$url = static fn (float $page): string => IriHelper::createIri($parsed['parts'], $parameters, $pageParameterName, $page, $urlGenerationStrategy);

// REFACTOR-WHEN: a third consumer of the page-link math appears → move Hydra's PaginationHelperTrait to State\Util and share it
$currentPage = $paginator->getCurrentPage();
$lastPage = $paginator instanceof PaginatorInterface ? $paginator->getLastPage() : null;

$pages = ['self' => $currentPage];
if (null !== $lastPage) {
$pages['first'] = 1.;
}
if ($currentPage > 1.) {
$pages['prev'] = $currentPage - 1.;
}
if ((null !== $lastPage && $currentPage < $lastPage) || (null === $lastPage && \count($paginator) >= $itemsPerPage)) {
$pages['next'] = $currentPage + 1.;
}
if (null !== $lastPage) {
$pages['last'] = $lastPage;
}

$linkProvider = $request->attributes->get('_api_platform_links') ?? new GenericLinkProvider();
foreach ($pages as $rel => $page) {
$linkProvider = $linkProvider->withLink(new Link($rel, $url($page)));
}
$request->attributes->set('_api_platform_links', $linkProvider);

return $this->decorated->process($data, $operation, $uriVariables, $context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ private function registerCommonConfiguration(ContainerBuilder $container, array
$loader->load('state/processor.php');
}
$loader->load('state/parameter_provider.php');
$loader->load('state/pagination_link.php');

$container->setParameter('api_platform.enable_entrypoint', $config['enable_entrypoint']);
$container->setParameter('api_platform.enable_docs', $config['enable_docs']);
Expand Down
Loading
Loading