Skip to content

Commit c91971d

Browse files
committed
feat(metadata): use uri RFC 3986 on PHP8.5
1 parent 325a04c commit c91971d

File tree

10 files changed

+992
-48
lines changed

10 files changed

+992
-48
lines changed

src/Hal/Serializer/CollectionNormalizer.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,22 @@ protected function getPaginationData(iterable $object, array $context = []): arr
4747

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

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

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

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

src/Hydra/Serializer/CollectionFiltersNormalizer.php

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
2626
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
2727
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
28+
use Uri\Rfc3986\Uri;
2829

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

97-
$requestParts = parse_url($context['request_uri'] ?? '');
98-
if (!\is_array($requestParts)) {
99-
return $data;
98+
$requestUri = $context['request_uri'] ?? '';
99+
if (PHP_VERSION_ID >= 80500 && \class_exists(Uri::class)) {
100+
if (null === $uri = Uri::parse($requestUri)) {
101+
return $data;
102+
}
103+
104+
$path = $uri->getPath();
105+
} else {
106+
$requestParts = parse_url($requestUri);
107+
if (!\is_array($requestParts)) {
108+
return $data;
109+
}
110+
111+
$path = $requestParts['path'] ?? null;
100112
}
113+
101114
$currentFilters = [];
102115
foreach ($resourceFilters as $filterId) {
103116
if ($filter = $this->getFilter($filterId)) {
@@ -112,7 +125,7 @@ public function normalize(mixed $object, ?string $format = null, array $context
112125
['mapping' => $mapping, 'keys' => $keys] = $this->getSearchMappingAndKeys($operation, $resourceClass, $currentFilters, $parameters, [$this, 'getFilter']);
113126
$data[$hydraPrefix.'search'] = [
114127
'@type' => $hydraPrefix.'IriTemplate',
115-
$hydraPrefix.'template' => \sprintf('%s{?%s}', $requestParts['path'], implode(',', $keys)),
128+
$hydraPrefix.'template' => \sprintf('%s{?%s}', $path, implode(',', $keys)),
116129
$hydraPrefix.'variableRepresentation' => 'BasicRepresentation',
117130
$hydraPrefix.'mapping' => $this->convertMappingToArray($mapping),
118131
];

src/Hydra/Serializer/PartialCollectionViewNormalizer.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,14 @@ private function populateDataWithCursorBasedPagination(array $data, array $parse
171171
$firstObject = current($objects);
172172
$lastObject = end($objects);
173173

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

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

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

184184
return $data;

src/Hydra/State/JsonStreamerProcessor.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use Symfony\Component\HttpFoundation\StreamedResponse;
3434
use Symfony\Component\JsonStreamer\StreamWriterInterface;
3535
use Symfony\Component\TypeInfo\Type;
36+
use Uri\Rfc3986\Uri;
3637

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

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

9091
if ($data instanceof PaginatorInterface) {

src/Hydra/State/Util/PaginationHelperTrait.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ private function getPaginationIri(array $parsed, ?float $currentPage, ?float $la
2626
$first = $last = $previous = $next = null;
2727

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

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

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

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

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

7070
if (!$paginated && $appliedFilters) {
7171
return new PartialCollectionView($id);

src/JsonApi/Serializer/CollectionNormalizer.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,22 +48,22 @@ protected function getPaginationData(iterable $object, array $context = []): arr
4848

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

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

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

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

src/Metadata/Tests/Util/IriHelperTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\Metadata\UrlGeneratorInterface;
1818
use ApiPlatform\Metadata\Util\IriHelper;
1919
use PHPUnit\Framework\TestCase;
20+
use Uri\Rfc3986\Uri;
2021

2122
/**
2223
* @author Kévin Dunglas <dunglas@gmail.com>
@@ -25,6 +26,10 @@ class IriHelperTest extends TestCase
2526
{
2627
public function testHelpers(): void
2728
{
29+
if (PHP_VERSION_ID >= 80500 && class_exists(Uri::class)) {
30+
self::markTestSkipped('Parsing url with former "parse_url()" method is not available after PHP8.5 and ext-uri');
31+
}
32+
2833
$parsed = [
2934
'parts' => [
3035
'path' => '/hello.json',
@@ -70,6 +75,25 @@ public function testHelpersWithNetworkPath(): void
7075
$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));
7176
}
7277

78+
public function testHelpersWithRFC3986(): void
79+
{
80+
if (PHP_VERSION_ID < 80500 || !class_exists(Uri::class)) {
81+
self::markTestSkipped('RFC 3986 URI parser needs PHP 8.5 or higher and php-uri extension.');
82+
}
83+
84+
$parsed = [
85+
'uri' => new Uri('/hello.json?foo=bar&page=2&bar=3'),
86+
'parameters' => [
87+
'foo' => 'bar',
88+
'bar' => '3',
89+
],
90+
];
91+
92+
93+
$this->assertEquals($parsed, IriHelper::parseIri('/hello.json?foo=bar&page=2&bar=3', 'page'));
94+
$this->assertSame('/hello.json?foo=bar&bar=3&page=2', IriHelper::createIri($parsed['uri'], $parsed['parameters'], 'page', 2.));
95+
}
96+
7397
public function testParseIriWithInvalidUrl(): void
7498
{
7599
$this->expectException(InvalidArgumentException::class);

src/Metadata/Util/IriHelper.php

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
1717
use ApiPlatform\Metadata\UrlGeneratorInterface;
1818
use ApiPlatform\State\Util\RequestParser;
19+
use Uri\InvalidUriException;
20+
use Uri\Rfc3986\Uri;
1921

2022
/**
2123
* Parses and creates IRIs.
@@ -30,12 +32,35 @@ private function __construct()
3032
{
3133
}
3234

35+
public static function parseIri(string $iri, string $pageParameterName): array
36+
{
37+
if (PHP_VERSION_ID < 80500 || !\class_exists(Uri::class)) {
38+
return self::parseLegacyIri($iri, $pageParameterName);
39+
}
40+
41+
try {
42+
$uri = new Uri($iri);
43+
} catch (InvalidUriException $e) {
44+
throw new InvalidArgumentException(\sprintf('The request URI "%s" is malformed.', $iri), previous: $e);
45+
}
46+
47+
$parameters = [];
48+
if (null !== $query = $uri->getQuery()) {
49+
$parameters = RequestParser::parseRequestParams($query);
50+
51+
// Remove existing page parameter
52+
unset($parameters[$pageParameterName]);
53+
}
54+
55+
return ['uri' => $uri, 'parameters' => $parameters];
56+
}
57+
3358
/**
3459
* Parses and standardizes the request IRI.
3560
*
3661
* @throws InvalidArgumentException
3762
*/
38-
public static function parseIri(string $iri, string $pageParameterName): array
63+
private static function parseLegacyIri(string $iri, string $pageParameterName): array
3964
{
4065
$parts = parse_url($iri);
4166
if (false === $parts) {
@@ -58,14 +83,29 @@ public static function parseIri(string $iri, string $pageParameterName): array
5883
*
5984
* @param int $urlGenerationStrategy
6085
*/
61-
public static function createIri(array $parts, array $parameters, ?string $pageParameterName = null, ?float $page = null, $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH): string
86+
public static function createIri(array|Uri $parts, array $parameters, ?string $pageParameterName = null, ?float $page = null, $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH): string
6287
{
6388
if (null !== $page && null !== $pageParameterName) {
6489
$parameters[$pageParameterName] = $page;
6590
}
6691

6792
$query = http_build_query($parameters, '', '&', \PHP_QUERY_RFC3986);
68-
$parts['query'] = preg_replace('/%5B\d+%5D/', '%5B%5D', $query);
93+
$queryParts = preg_replace('/%5B\d+%5D/', '%5B%5D', $query);
94+
95+
if ($parts instanceof Uri) {
96+
$uri = $parts
97+
->withQuery('' !== $queryParts ? $queryParts : null)
98+
->withScheme(UrlGeneratorInterface::ABS_URL === $urlGenerationStrategy && null === $parts->getScheme() ? ($parts->getPort() === 443 ? 'https' : 'http') : null)
99+
;
100+
101+
if (null === $urlGenerationStrategy || UrlGeneratorInterface::NET_PATH === $urlGenerationStrategy) {
102+
$uri = $uri->withScheme(null)->withUserInfo(null)->withHost(null)->withPort(null);
103+
}
104+
105+
return $uri->toString();
106+
}
107+
108+
$parts['query'] = $queryParts;
69109

70110
$url = '';
71111
if ((UrlGeneratorInterface::ABS_URL === $urlGenerationStrategy || UrlGeneratorInterface::NET_PATH === $urlGenerationStrategy) && isset($parts['host'])) {

src/State/Util/HttpResponseHeadersTrait.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Symfony\Component\HttpFoundation\Request;
2727
use Symfony\Component\HttpFoundation\Response;
2828
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface;
29+
use Uri\Rfc3986\Uri;
2930

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

100-
$requestParts = parse_url($request->getRequestUri());
101+
$query = PHP_VERSION_ID >= 80500 && \class_exists(Uri::class)
102+
? Uri::parse($context['request_uri'] ?? '')?->getQuery()
103+
: $requestParts['query'] ?? null
104+
;
105+
101106
if ($this->iriConverter && !isset($headers['Content-Location'])) {
102107
try {
103108
$iri = null;
@@ -109,8 +114,8 @@ private function getHeaders(Request $request, HttpOperation $operation, array $c
109114

110115
if ($iri && 'GET' !== $method) {
111116
$location = \sprintf('%s.%s', $iri, $request->getRequestFormat());
112-
if (isset($requestParts['query'])) {
113-
$location .= '?'.$requestParts['query'];
117+
if (isset($query)) {
118+
$location .= '?'.$query;
114119
}
115120

116121
$headers['Content-Location'] = $location;

0 commit comments

Comments
 (0)