Skip to content
Draft
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
10 changes: 5 additions & 5 deletions src/Hal/Serializer/CollectionNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,22 @@ protected function getPaginationData(iterable $object, array $context = []): arr

$data = [
'_links' => [
'self' => ['href' => IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null, $urlGenerationStrategy)],
'self' => ['href' => IriHelper::createIri($parsed['uri'] ?? $parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null, $urlGenerationStrategy)],
],
];

if ($paginated) {
if (null !== $lastPage) {
$data['_links']['first']['href'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1., $urlGenerationStrategy);
$data['_links']['last']['href'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage, $urlGenerationStrategy);
$data['_links']['first']['href'] = IriHelper::createIri($parsed['uri'] ?? $parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1., $urlGenerationStrategy);
$data['_links']['last']['href'] = IriHelper::createIri($parsed['uri'] ?? $parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage, $urlGenerationStrategy);
}

if (1. !== $currentPage) {
$data['_links']['prev']['href'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1., $urlGenerationStrategy);
$data['_links']['prev']['href'] = IriHelper::createIri($parsed['uri'] ?? $parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1., $urlGenerationStrategy);
}

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

Expand Down
21 changes: 17 additions & 4 deletions src/Hydra/Serializer/CollectionFiltersNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Uri\Rfc3986\Uri;

/**
* Enhances the result of collection by adding the filters applied on collection.
Expand Down Expand Up @@ -94,10 +95,22 @@ public function normalize(mixed $object, ?string $format = null, array $context
return $data;
}

$requestParts = parse_url($context['request_uri'] ?? '');
if (!\is_array($requestParts)) {
return $data;
$requestUri = $context['request_uri'] ?? '';
if (PHP_VERSION_ID >= 80500 && \class_exists(Uri::class)) {
if (null === $uri = Uri::parse($requestUri)) {
return $data;
}

$path = $uri->getPath();
} else {
$requestParts = parse_url($requestUri);
if (!\is_array($requestParts)) {
return $data;
}

$path = $requestParts['path'] ?? null;
}

$currentFilters = [];
foreach ($resourceFilters as $filterId) {
if ($filter = $this->getFilter($filterId)) {
Expand All @@ -112,7 +125,7 @@ public function normalize(mixed $object, ?string $format = null, array $context
['mapping' => $mapping, 'keys' => $keys] = $this->getSearchMappingAndKeys($operation, $resourceClass, $currentFilters, $parameters, [$this, 'getFilter']);
$data[$hydraPrefix.'search'] = [
'@type' => $hydraPrefix.'IriTemplate',
$hydraPrefix.'template' => \sprintf('%s{?%s}', $requestParts['path'], implode(',', $keys)),
$hydraPrefix.'template' => \sprintf('%s{?%s}', $path, implode(',', $keys)),
$hydraPrefix.'variableRepresentation' => 'BasicRepresentation',
$hydraPrefix.'mapping' => $this->convertMappingToArray($mapping),
];
Expand Down
6 changes: 3 additions & 3 deletions src/Hydra/Serializer/PartialCollectionViewNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,14 @@ private function populateDataWithCursorBasedPagination(array $data, array $parse
$firstObject = current($objects);
$lastObject = end($objects);

$data[$hydraPrefix.'view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], urlGenerationStrategy: $urlGenerationStrategy);
$data[$hydraPrefix.'view']['@id'] = IriHelper::createIri($parsed['uri'] ?? $parsed['parts'], $parsed['parameters'], urlGenerationStrategy: $urlGenerationStrategy);

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

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

return $data;
Expand Down
5 changes: 3 additions & 2 deletions src/Hydra/State/JsonStreamerProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\JsonStreamer\StreamWriterInterface;
use Symfony\Component\TypeInfo\Type;
use Uri\Rfc3986\Uri;

/**
* @implements ProcessorInterface<mixed,mixed>
Expand Down Expand Up @@ -83,8 +84,8 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
$collection->view = $this->getPartialCollectionView($data, $requestUri, $this->pageParameterName, $this->enabledParameterName, $operation->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);

if ($operation->getParameters()) {
$parts = parse_url($requestUri);
$collection->search = $this->getSearch($parts['path'] ?? '', $operation);
$path = PHP_VERSION_ID >= 80500 && \class_exists(Uri::class) ? Uri::parse($requestUri)?->getPath() : ($parts['path'] ?? '');
$collection->search = $this->getSearch($path, $operation);
}

if ($data instanceof PaginatorInterface) {
Expand Down
10 changes: 5 additions & 5 deletions src/Hydra/State/Util/PaginationHelperTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,16 @@ private function getPaginationIri(array $parsed, ?float $currentPage, ?float $la
$first = $last = $previous = $next = null;

if (null !== $lastPage) {
$first = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $pageParameterName, 1., $urlGenerationStrategy);
$last = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $pageParameterName, $lastPage, $urlGenerationStrategy);
$first = IriHelper::createIri($parsed['uri'] ?? $parsed['parts'], $parsed['parameters'], $pageParameterName, 1., $urlGenerationStrategy);
$last = IriHelper::createIri($parsed['uri'] ?? $parsed['parts'], $parsed['parameters'], $pageParameterName, $lastPage, $urlGenerationStrategy);
}

if (1. !== $currentPage) {
$previous = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $pageParameterName, $currentPage - 1., $urlGenerationStrategy);
$previous = IriHelper::createIri($parsed['uri'] ?? $parsed['parts'], $parsed['parameters'], $pageParameterName, $currentPage - 1., $urlGenerationStrategy);
}

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

return [
Expand Down Expand Up @@ -65,7 +65,7 @@ private function getPartialCollectionView(mixed $object, string $requestUri, str
$appliedFilters = $parsed['parameters'];
unset($appliedFilters[$enabledParameterName]);

$id = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $pageParameterName, $paginated ? $currentPage : null, $urlGenerationStrategy);
$id = IriHelper::createIri($parsed['uri'] ?? $parsed['parts'], $parsed['parameters'], $pageParameterName, $paginated ? $currentPage : null, $urlGenerationStrategy);

if (!$paginated && $appliedFilters) {
return new PartialCollectionView($id);
Expand Down
10 changes: 5 additions & 5 deletions src/JsonApi/Serializer/CollectionNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,22 @@ protected function getPaginationData(iterable $object, array $context = []): arr

$data = [
'links' => [
'self' => IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null, $urlGenerationStrategy),
'self' => IriHelper::createIri($parsed['uri'] ?? $parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null, $urlGenerationStrategy),
],
];

if ($paginated) {
if (null !== $lastPage) {
$data['links']['first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1., $urlGenerationStrategy);
$data['links']['last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage, $urlGenerationStrategy);
$data['links']['first'] = IriHelper::createIri($parsed['uri'] ?? $parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1., $urlGenerationStrategy);
$data['links']['last'] = IriHelper::createIri($parsed['uri'] ?? $parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage, $urlGenerationStrategy);
}

if (1. !== $currentPage) {
$data['links']['prev'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1., $urlGenerationStrategy);
$data['links']['prev'] = IriHelper::createIri($parsed['uri'] ?? $parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1., $urlGenerationStrategy);
}

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

Expand Down
55 changes: 55 additions & 0 deletions src/Metadata/Tests/Util/IriHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use ApiPlatform\Metadata\UrlGeneratorInterface;
use ApiPlatform\Metadata\Util\IriHelper;
use PHPUnit\Framework\TestCase;
use Uri\Rfc3986\Uri;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
Expand All @@ -25,6 +26,10 @@ class IriHelperTest extends TestCase
{
public function testHelpers(): void
{
if (PHP_VERSION_ID >= 80500 && class_exists(Uri::class)) {
self::markTestSkipped('Parsing url with former "parse_url()" method is not available after PHP8.5 and ext-uri');
}

$parsed = [
'parts' => [
'path' => '/hello.json',
Expand Down Expand Up @@ -70,6 +75,56 @@ public function testHelpersWithNetworkPath(): void
$this->assertSame('//foo:bar@localhost:443/hello.json?foo=bar&bar=3&page=2#foo', IriHelper::createIri($parsed['parts'], $parsed['parameters'], 'page', 2., UrlGeneratorInterface::NET_PATH));
}

public function testHelpersWithRFC3986(): void
{
if (PHP_VERSION_ID < 80500 || !class_exists(Uri::class)) {
self::markTestSkipped('RFC 3986 URI parser needs PHP 8.5 or higher and php-uri extension.');
}

$parsed = [
'uri' => new Uri('/hello.json?foo=bar&page=2&bar=3'),
'parameters' => [
'foo' => 'bar',
'bar' => '3',
],
];


$this->assertEquals($parsed, IriHelper::parseIri('/hello.json?foo=bar&page=2&bar=3', 'page'));
$this->assertSame('/hello.json?foo=bar&bar=3&page=2', IriHelper::createIri($parsed['uri'], $parsed['parameters'], 'page', 2.));
}

public function testHelpersWithNetworkPathAndRFC3986(): void
{
if (PHP_VERSION_ID < 80500 || !class_exists(Uri::class)) {
self::markTestSkipped('RFC 3986 URI parser needs PHP 8.5 or higher and php-uri extension.');
}

$parsed = [
'uri' => $uri = new Uri('/hello.json')
->withQuery('foo=bar&page=2&bar=3')
->withScheme('http')
->withHost('localhost')
->withUserInfo('foo:bar')
->withPort(8080)
->withFragment('foo'),
'parameters' => [
'foo' => 'bar',
'bar' => '3',
],
];

$this->assertSame('//foo:bar@localhost:8080/hello.json?foo=bar&bar=3&page=2#foo', IriHelper::createIri($parsed['uri'], $parsed['parameters'], 'page', 2., UrlGeneratorInterface::NET_PATH));

$parsed['uri'] = $uri->withScheme(null);

$this->assertSame('//foo:bar@localhost:8080/hello.json?foo=bar&bar=3&page=2#foo', IriHelper::createIri($parsed['uri'], $parsed['parameters'], 'page', 2., UrlGeneratorInterface::NET_PATH));

$parsed['uri'] = $uri->withPort(443);

$this->assertSame('//foo:bar@localhost:443/hello.json?foo=bar&bar=3&page=2#foo', IriHelper::createIri($parsed['uri'], $parsed['parameters'], 'page', 2., UrlGeneratorInterface::NET_PATH));
}

public function testParseIriWithInvalidUrl(): void
{
$this->expectException(InvalidArgumentException::class);
Expand Down
46 changes: 43 additions & 3 deletions src/Metadata/Util/IriHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use ApiPlatform\State\Util\RequestParser;
use Uri\InvalidUriException;
use Uri\Rfc3986\Uri;

/**
* Parses and creates IRIs.
Expand All @@ -30,12 +32,35 @@ private function __construct()
{
}

public static function parseIri(string $iri, string $pageParameterName): array
{
if (PHP_VERSION_ID < 80500 || !\class_exists(Uri::class)) {
return self::parseLegacyIri($iri, $pageParameterName);
}

try {
$uri = new Uri($iri);
} catch (InvalidUriException $e) {
throw new InvalidArgumentException(\sprintf('The request URI "%s" is malformed.', $iri), previous: $e);
}

$parameters = [];
if (null !== $query = $uri->getQuery()) {
$parameters = RequestParser::parseRequestParams($query);

// Remove existing page parameter
unset($parameters[$pageParameterName]);
}

return ['uri' => $uri, 'parameters' => $parameters];
}

/**
* Parses and standardizes the request IRI.
*
* @throws InvalidArgumentException
*/
public static function parseIri(string $iri, string $pageParameterName): array
private static function parseLegacyIri(string $iri, string $pageParameterName): array
{
$parts = parse_url($iri);
if (false === $parts) {
Expand All @@ -58,14 +83,29 @@ public static function parseIri(string $iri, string $pageParameterName): array
*
* @param int $urlGenerationStrategy
*/
public static function createIri(array $parts, array $parameters, ?string $pageParameterName = null, ?float $page = null, $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH): string
public static function createIri(array|Uri $parts, array $parameters, ?string $pageParameterName = null, ?float $page = null, $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH): string
{
if (null !== $page && null !== $pageParameterName) {
$parameters[$pageParameterName] = $page;
}

$query = http_build_query($parameters, '', '&', \PHP_QUERY_RFC3986);
$parts['query'] = preg_replace('/%5B\d+%5D/', '%5B%5D', $query);
$queryParts = preg_replace('/%5B\d+%5D/', '%5B%5D', $query);

if ($parts instanceof Uri) {
$uri = $parts
->withQuery('' !== $queryParts ? $queryParts : null)
->withScheme(UrlGeneratorInterface::ABS_URL === $urlGenerationStrategy && null === $parts->getScheme() ? ($parts->getPort() === 443 ? 'https' : 'http') : null)
;

if (null === $urlGenerationStrategy) {
$uri = $uri->withScheme(null)->withUserInfo(null)->withPort(null)->withHost(null);
}

return $uri->toString();
}

$parts['query'] = $queryParts;

$url = '';
if ((UrlGeneratorInterface::ABS_URL === $urlGenerationStrategy || UrlGeneratorInterface::NET_PATH === $urlGenerationStrategy) && isset($parts['host'])) {
Expand Down
11 changes: 8 additions & 3 deletions src/State/Util/HttpResponseHeadersTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface;
use Uri\Rfc3986\Uri;

/**
* Shares the logic to create API Platform's headers.
Expand Down Expand Up @@ -97,7 +98,11 @@ private function getHeaders(Request $request, HttpOperation $operation, array $c
}
}

$requestParts = parse_url($request->getRequestUri());
$query = PHP_VERSION_ID >= 80500 && \class_exists(Uri::class)
? Uri::parse($context['request_uri'] ?? '')?->getQuery()
: $requestParts['query'] ?? null
;

if ($this->iriConverter && !isset($headers['Content-Location'])) {
try {
$iri = null;
Expand All @@ -109,8 +114,8 @@ private function getHeaders(Request $request, HttpOperation $operation, array $c

if ($iri && 'GET' !== $method) {
$location = \sprintf('%s.%s', $iri, $request->getRequestFormat());
if (isset($requestParts['query'])) {
$location .= '?'.$requestParts['query'];
if (isset($query)) {
$location .= '?'.$query;
}

$headers['Content-Location'] = $location;
Expand Down
Loading
Loading