Skip to content

Commit

Permalink
feat: link headers
Browse files Browse the repository at this point in the history
  • Loading branch information
priyadi committed Sep 7, 2024
1 parent 6680c7a commit 35746ea
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 0 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"doctrine/persistence": "^3.1",
"ekino/phpstan-banned-code": "^1.0 || ^2.0",
"fakerphp/faker": "^1.23",
"graviton/link-header-rel-parser": "^1.0",
"phpstan/phpstan": "^1.10.66 || ^1.11",
"phpstan/phpstan-deprecation-rules": "^1.1",
"phpstan/phpstan-phpunit": "^1.3",
Expand Down
8 changes: 8 additions & 0 deletions packages/rekapager-bundle/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Rekalogika\Rekapager\Bundle\Contracts\PageUrlGeneratorFactoryInterface;
use Rekalogika\Rekapager\Bundle\Implementation\SymfonyPageUrlGeneratorFactory;
use Rekalogika\Rekapager\Bundle\PagerFactory;
use Rekalogika\Rekapager\Bundle\RekapagerLinkProcessor;
use Rekalogika\Rekapager\Bundle\Twig\RekapagerExtension;
use Rekalogika\Rekapager\Bundle\Twig\RekapagerRuntime;
use Rekalogika\Rekapager\Bundle\Twig\TwigPagerRenderer;
Expand Down Expand Up @@ -71,6 +72,13 @@
'$defaultUrlReferenceType' => '%rekalogika.rekapager.config.default_url_reference_type%',
]);

$services
->set(RekapagerLinkProcessor::class)
->decorate('api_platform.state_processor.respond', priority: 410)
->args([
service('.inner'),
]);

$services
->set(TwigPagerRenderer::class)
->args([
Expand Down
101 changes: 101 additions & 0 deletions packages/rekapager-bundle/src/RekapagerLinkProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

/*
* This file is part of rekalogika/rekapager package.
*
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
*
* For the full copyright and license information, please view the LICENSE file
* that was distributed with this source code.
*/

namespace Rekalogika\Rekapager\Bundle;

use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use Psr\Link\EvolvableLinkProviderInterface;
use Rekalogika\Rekapager\Contracts\PagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\WebLink\GenericLinkProvider;
use Symfony\Component\WebLink\Link;

/**
* @template T1
* @template T2
*
* @implements ProcessorInterface<T1,T2>
*/
class RekapagerLinkProcessor implements ProcessorInterface
{
/**
* @param ProcessorInterface<T1,T2> $decorated
*/
public function __construct(
private readonly ProcessorInterface $decorated,
) {}

public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
{
if (
!($request = $context['request'] ?? null)
|| !$request instanceof Request
|| !$operation instanceof HttpOperation
|| $this->isPreflightRequest($request)
) {
/** @psalm-suppress MixedArgumentTypeCoercion */
return $this->decorated->process($data, $operation, $uriVariables, $context);
}

$linkProvider = $request->attributes->get('_api_platform_links') ?? new GenericLinkProvider();

if (!$linkProvider instanceof EvolvableLinkProviderInterface) {
/** @psalm-suppress MixedArgumentTypeCoercion */
return $this->decorated->process($data, $operation, $uriVariables, $context);
}

/** @var mixed */
$requestData = $request->attributes->get('data');

if (!$requestData instanceof PagerInterface) {
/** @psalm-suppress MixedArgumentTypeCoercion */
/** @psalm-suppress MixedArgument */
return $this->decorated->process($data, $operation, $uriVariables, $context);
}

if (null !== $first = $requestData->getFirstPage()?->getUrl()) {
$linkProvider = $linkProvider->withLink(new Link('first', $first));
}

if (null !== $prev = $requestData->getPreviousPage()?->getUrl()) {
$linkProvider = $linkProvider->withLink(new Link('prev', $prev));
}

if (null !== $next = $requestData->getNextPage()?->getUrl()) {
$linkProvider = $linkProvider->withLink(new Link('next', $next));
}

if (null !== $last = $requestData->getLastPage()?->getUrl()) {
$linkProvider = $linkProvider->withLink(new Link('last', $last));
}

$request->attributes->set('_api_platform_links', $linkProvider);

/**
* @psalm-suppress MixedArgumentTypeCoercion
* @psalm-suppress InvalidArgument
* @phpstan-ignore argument.type
*/
return $this->decorated->process($data, $operation, $uriVariables, $context);
}

/**
* @see ApiPlatform\State\Util\CorsTrait::isPreflightRequest()
*/
private function isPreflightRequest(Request $request): bool
{
return $request->isMethod('OPTIONS') && $request->headers->has('Access-Control-Request-Method');
}
}
21 changes: 21 additions & 0 deletions tests/src/IntegrationTests/ApiPlatform/ApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Rekalogika\Rekapager\Tests\IntegrationTests\ApiPlatform;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use Graviton\LinkHeaderParser\LinkHeader;

class ApiTest extends ApiTestCase
{
Expand Down Expand Up @@ -75,6 +76,16 @@ public function testApiWithCustomProvider(): void
$lastPage = $response->toArray()['hydra:view']['hydra:last'] ?? null;
self::assertIsString($lastPage);

$headers = $response->getHeaders();
$link = $headers['link'][0] ?? null;
self::assertNotNull($link);
$linkHeader = LinkHeader::fromString($link);

self::assertEquals($firstPage, $linkHeader->getRel('first')?->getUri());
self::assertEquals($previousPage, $linkHeader->getRel('prev')?->getUri());
self::assertEquals($nextPage, $linkHeader->getRel('next')?->getUri());
self::assertEquals($lastPage, $linkHeader->getRel('last')?->getUri());

// test last page

$response = $client->request('GET', $lastPage);
Expand Down Expand Up @@ -104,5 +115,15 @@ public function testApiWithCustomProvider(): void
/** @var ?string */
$lastPage = $response->toArray()['hydra:view']['hydra:last'] ?? null;
self::assertNull($lastPage);

$headers = $response->getHeaders();
$link = $headers['link'][0] ?? null;
self::assertNotNull($link);
$linkHeader = LinkHeader::fromString($link);

self::assertEquals($firstPage, $linkHeader->getRel('first')?->getUri());
self::assertEquals($previousPage, $linkHeader->getRel('prev')?->getUri());
self::assertEquals($nextPage, $linkHeader->getRel('next')?->getUri());
self::assertEquals($lastPage, $linkHeader->getRel('last')?->getUri());
}
}

0 comments on commit 35746ea

Please sign in to comment.