Skip to content

Commit 2819d56

Browse files
authored
fix(hydra): hydra:view with absolute iris (#6208)
* fix(hydra): hydra:view with absolute iris fixes #6096
1 parent 98f4b8f commit 2819d56

File tree

6 files changed

+50
-23
lines changed

6 files changed

+50
-23
lines changed

features/doctrine/multiple_filter.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Feature: Multiple filters on collections
3838
"hydra:view": {
3939
"type": "object",
4040
"properties": {
41-
"@id": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-28&dummyBoolean=1$"},
41+
"@id": {"pattern": "^/dummies\\?dummyBoolean=1&dummyDate%5Bafter%5D=2015-04-28$"},
4242
"@type": {"pattern": "^hydra:PartialCollectionView$"}
4343
}
4444
}

features/elasticsearch/match_filter.feature

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ Feature: Match filter on collections from Elasticsearch
209209
"hydra:view": {
210210
"type": "object",
211211
"properties": {
212-
"@id": {"pattern": "^/tweets\\?message%5B%5D=Good%20job&message%5B%5D=run&author.firstName=Caroline$"},
212+
"@id": {"pattern": "^/tweets\\?author.firstName=Caroline&message%5B%5D=Good%20job&message%5B%5D=run$"},
213213
"@type": {"pattern": "^hydra:PartialCollectionView$"}
214214
}
215215
}
@@ -422,7 +422,7 @@ Feature: Match filter on collections from Elasticsearch
422422
"hydra:view": {
423423
"type": "object",
424424
"properties": {
425-
"@id": {"pattern": "^/books\\?message%5B%5D=Good%20job&message%5B%5D=run&library.firstName=Caroline$"},
425+
"@id": {"pattern": "^/books\\?library.firstName=Caroline&message%5B%5D=Good%20job&message%5B%5D=run$"},
426426
"@type": {"pattern": "^hydra:PartialCollectionView$"}
427427
}
428428
}

features/hydra/absolute.feature

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Feature: Collections with absolute IRIs support
2+
In order to retrieve large collections of resources
3+
As a client software developer
4+
I need to retrieve paged collections respecting the Hydra specification and with absolute iris
5+
6+
@createSchema
7+
Scenario: Retrieve third page of collection with absolute iris
8+
Given there are 30 absoluteUrlDummy objects with a related absoluteUrlRelationDummy
9+
When I send a "GET" request to "/absolute_url_dummies?page=3"
10+
Then the response status code should be 200
11+
And the response should be in JSON
12+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
13+
And the JSON node "hydra:view" should be equal to:
14+
"""
15+
{
16+
"@id": "http://example.com/absolute_url_dummies?page=3",
17+
"@type": "hydra:PartialCollectionView",
18+
"hydra:first": "http://example.com/absolute_url_dummies?page=1",
19+
"hydra:last": "http://example.com/absolute_url_dummies?page=10",
20+
"hydra:previous": "http://example.com/absolute_url_dummies?page=2",
21+
"hydra:next": "http://example.com/absolute_url_dummies?page=4"
22+
}
23+
"""

features/hydra/collection.feature

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -576,10 +576,10 @@ Feature: Collections support
576576
"hydra:view": {
577577
"type": "object",
578578
"properties": {
579-
"@id": {"pattern": "^/so_manies\\?order%5Bid%5D=desc&id%5Bgt%5D=10$"},
579+
"@id": {"pattern": "^/so_manies\\?id%5Bgt%5D=10&order%5Bid%5D=desc$"},
580580
"@type": {"pattern": "^hydra:PartialCollectionView$"},
581-
"hydra:previous": {"pattern": "^/so_manies\\?order%5Bid%5D=desc&id%5Bgt%5D=13$"},
582-
"hydra:next": {"pattern": "^/so_manies\\?order%5Bid%5D=desc&id%5Blt%5D=10$"}
581+
"hydra:previous": {"pattern": "^/so_manies\\?id%5Bgt%5D=13&order%5Bid%5D=desc$"},
582+
"hydra:next": {"pattern": "^/so_manies\\?id%5Blt%5D=10&order%5Bid%5D=desc$"}
583583
},
584584
"additionalProperties": false
585585
}

src/Hydra/Serializer/PartialCollectionViewNormalizer.php

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\Metadata\HttpOperation;
1717
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
18+
use ApiPlatform\Metadata\UrlGeneratorInterface;
1819
use ApiPlatform\Serializer\CacheableSupportsMethodInterface;
1920
use ApiPlatform\State\Pagination\PaginatorInterface;
2021
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
@@ -37,7 +38,7 @@ final class PartialCollectionViewNormalizer implements NormalizerInterface, Norm
3738
{
3839
private readonly PropertyAccessorInterface $propertyAccessor;
3940

40-
public function __construct(private readonly NormalizerInterface $collectionNormalizer, private readonly string $pageParameterName = 'page', private string $enabledParameterName = 'pagination', private readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, ?PropertyAccessorInterface $propertyAccessor = null)
41+
public function __construct(private readonly NormalizerInterface $collectionNormalizer, private readonly string $pageParameterName = 'page', private string $enabledParameterName = 'pagination', private readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, ?PropertyAccessorInterface $propertyAccessor = null, private readonly int $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH)
4142
{
4243
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
4344
}
@@ -72,7 +73,7 @@ public function normalize(mixed $object, ?string $format = null, array $context
7273
// TODO: This needs to be changed as well as I wrote in the CollectionFiltersNormalizer
7374
// We should not rely on the request_uri but instead rely on the UriTemplate
7475
// This needs that we implement the RFC and that we do more parsing before calling the serialization (MainController)
75-
$parsed = IriHelper::parseIri($context['request_uri'] ?? '/', $this->pageParameterName);
76+
$parsed = IriHelper::parseIri($context['uri'] ?? $context['request_uri'] ?? '/', $this->pageParameterName);
7677
$appliedFilters = $parsed['parameters'];
7778
unset($appliedFilters[$this->enabledParameterName]);
7879

@@ -82,22 +83,24 @@ public function normalize(mixed $object, ?string $format = null, array $context
8283

8384
$isPaginatedWithCursor = false;
8485
$cursorPaginationAttribute = null;
85-
if ($this->resourceMetadataFactory && isset($context['resource_class']) && $paginated) {
86-
/** @var HttpOperation $operation */
86+
$operation = $context['operation'] ?? null;
87+
if (!$operation && $this->resourceMetadataFactory && isset($context['resource_class']) && $paginated) {
8788
$operation = $this->resourceMetadataFactory->create($context['resource_class'])->getOperation($context['operation_name'] ?? null);
88-
$isPaginatedWithCursor = [] !== $cursorPaginationAttribute = ($operation->getPaginationViaCursor() ?? []);
8989
}
9090

91+
$cursorPaginationAttribute = $operation instanceof HttpOperation ? $operation->getPaginationViaCursor() : null;
92+
$isPaginatedWithCursor = (bool) $cursorPaginationAttribute;
93+
9194
$data['hydra:view'] = ['@id' => null, '@type' => 'hydra:PartialCollectionView'];
9295

9396
if ($isPaginatedWithCursor) {
94-
return $this->populateDataWithCursorBasedPagination($data, $parsed, $object, $cursorPaginationAttribute);
97+
return $this->populateDataWithCursorBasedPagination($data, $parsed, $object, $cursorPaginationAttribute, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
9598
}
9699

97-
$data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null);
100+
$data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
98101

99102
if ($paginated) {
100-
return $this->populateDataWithPagination($data, $parsed, $currentPage, $lastPage, $itemsPerPage, $pageTotalItems);
103+
return $this->populateDataWithPagination($data, $parsed, $currentPage, $lastPage, $itemsPerPage, $pageTotalItems, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
101104
}
102105

103106
return $data;
@@ -165,38 +168,38 @@ private function cursorPaginationFields(array $fields, int $direction, $object):
165168
return $paginationFilters;
166169
}
167170

168-
private function populateDataWithCursorBasedPagination(array $data, array $parsed, \Traversable $object, ?array $cursorPaginationAttribute): array
171+
private function populateDataWithCursorBasedPagination(array $data, array $parsed, \Traversable $object, ?array $cursorPaginationAttribute, ?int $urlGenerationStrategy): array
169172
{
170173
$objects = iterator_to_array($object);
171174
$firstObject = current($objects);
172175
$lastObject = end($objects);
173176

174-
$data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters']);
177+
$data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], urlGenerationStrategy: $urlGenerationStrategy);
175178

176179
if (false !== $lastObject && \is_array($cursorPaginationAttribute)) {
177-
$data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, 1, $lastObject)));
180+
$data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, 1, $lastObject)), urlGenerationStrategy: $urlGenerationStrategy);
178181
}
179182

180183
if (false !== $firstObject && \is_array($cursorPaginationAttribute)) {
181-
$data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, -1, $firstObject)));
184+
$data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, -1, $firstObject)), urlGenerationStrategy: $urlGenerationStrategy);
182185
}
183186

184187
return $data;
185188
}
186189

187-
private function populateDataWithPagination(array $data, array $parsed, ?float $currentPage, ?float $lastPage, ?float $itemsPerPage, ?float $pageTotalItems): array
190+
private function populateDataWithPagination(array $data, array $parsed, ?float $currentPage, ?float $lastPage, ?float $itemsPerPage, ?float $pageTotalItems, ?int $urlGenerationStrategy): array
188191
{
189192
if (null !== $lastPage) {
190-
$data['hydra:view']['hydra:first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1.);
191-
$data['hydra:view']['hydra:last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage);
193+
$data['hydra:view']['hydra:first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1., $urlGenerationStrategy);
194+
$data['hydra:view']['hydra:last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage, $urlGenerationStrategy);
192195
}
193196

194197
if (1. !== $currentPage) {
195-
$data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1.);
198+
$data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1., $urlGenerationStrategy);
196199
}
197200

198201
if ((null !== $lastPage && $currentPage < $lastPage) || (null === $lastPage && $pageTotalItems >= $itemsPerPage)) {
199-
$data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1.);
202+
$data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1., $urlGenerationStrategy);
200203
}
201204

202205
return $data;

src/Symfony/Bundle/Resources/config/hydra.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
<argument>%api_platform.collection.pagination.enabled_parameter_name%</argument>
7171
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
7272
<argument type="service" id="api_platform.property_accessor" />
73+
<argument>%api_platform.url_generation_strategy%</argument>
7374
</service>
7475

7576
<service id="api_platform.hydra.normalizer.collection_filters" class="ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer" decorates="api_platform.hydra.normalizer.collection" public="false">

0 commit comments

Comments
 (0)