Skip to content

Commit 16b8111

Browse files
authored
Merge pull request #5779 from soyuka/merge
Merge 3.1
2 parents b58ec12 + a70be44 commit 16b8111

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1253
-122
lines changed

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## v3.1.15
4+
5+
### Bug fixes
6+
7+
* [07c9989eb](https://github.com/api-platform/core/commit/07c9989eb2717d8881801c843706194499b6c903) fix(metadata): notexposed no urivariables inheritance (#5765)
8+
* [8d04dcf5f](https://github.com/api-platform/core/commit/8d04dcf5f63c152ffa4e9ae00c8d6624c97f2855) fix(metadata): fix POST on subresource (#5734)
9+
* [a774f4c51](https://github.com/api-platform/core/commit/a774f4c51167dbbe585269f14a7c51a3f9e38c3c) fix(doctrine): searchfilter with nested custom identifiers (#5760)
10+
* [b7258ef38](https://github.com/api-platform/core/commit/b7258ef38302c92869ab23d0dc83a2cb411526a7) fix: error 500 on request with 'empty' accept headers, e.g. 'accept: 0' or 'accept: ' (#5767)
11+
312
## v3.1.14
413

514
### Bug fixes
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Feature: Get a subresource from inverse side that has no item operation
2+
3+
@!mongodb
4+
@createSchema
5+
Scenario: Get a subresource from inverse side that has no item operation
6+
Given there are logs on an event
7+
When I send a "GET" request to "/events/03af3507-271e-4cca-8eee-6244fb06e95b/logs"
8+
Then the response status code should be 200

features/doctrine/search_filter.feature

+19
Original file line numberDiff line numberDiff line change
@@ -1033,3 +1033,22 @@ Feature: Search filter on collections
10331033
Then the response status code should be 200
10341034
And the response should be in JSON
10351035
And the JSON node "hydra:totalItems" should be equal to 1
1036+
1037+
@!mongodb
1038+
@createSchema
1039+
Scenario: Search on nested sub-entity that doesn't use "id" as its ORM identifier
1040+
Given there is a dummy entity with a sub entity with id "stringId" and name "someName"
1041+
When I send a "GET" request to "/dummy_with_subresource?subEntity=/dummy_subresource/stringId"
1042+
Then the response status code should be 200
1043+
And the response should be in JSON
1044+
And the JSON node "hydra:totalItems" should be equal to 1
1045+
1046+
@!mongodb
1047+
@createSchema
1048+
Scenario: Filters can use UUIDs
1049+
Given there is a group object with uuid "61817181-0ecc-42fb-a6e7-d97f2ddcb344" and 2 users
1050+
And there is a group object with uuid "32510d53-f737-4e70-8d9d-58e292c871f8" and 1 users
1051+
When I send a "GET" request to "/issue5735/issue5735_users?groups[]=/issue5735/groups/61817181-0ecc-42fb-a6e7-d97f2ddcb344&groups[]=/issue5735/groups/32510d53-f737-4e70-8d9d-58e292c871f8"
1052+
Then the response status code should be 200
1053+
And the response should be in JSON
1054+
And the JSON node "hydra:totalItems" should be equal to 3

features/hydra/item_uri_template.feature

+63-2
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,7 @@ Feature: Exposing a collection of objects should use the specified operation to
131131
"""
132132

133133
Scenario: Get a collection referencing another resource for its IRI
134-
When I add "Content-Type" header equal to "application/json"
135-
And I send a "GET" request to "/item_referenced_in_collection"
134+
When I send a "GET" request to "/item_referenced_in_collection"
136135
Then the response status code should be 200
137136
And the response should be in JSON
138137
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
@@ -159,3 +158,65 @@ Feature: Exposing a collection of objects should use the specified operation to
159158
"hydra:totalItems":2
160159
}
161160
"""
161+
162+
Scenario: Get a collection referencing an itemUriTemplate
163+
When I send a "GET" request to "/issue5662/books/a/reviews"
164+
Then the response status code should be 200
165+
And the response should be in JSON
166+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
167+
And the JSON should be equal to:
168+
"""
169+
{
170+
"@context":"/contexts/Review",
171+
"@id":"/issue5662/books/a/reviews",
172+
"@type":"hydra:Collection",
173+
"hydra:member":[
174+
{
175+
"@id":"/issue5662/books/a/reviews/1",
176+
"@type":"Review",
177+
"book":"/issue5662/books/a",
178+
"id":1,
179+
"body":"Best book ever!"
180+
},
181+
{
182+
"@id":"/issue5662/books/b/reviews/2",
183+
"@type":"Review",
184+
"book":"/issue5662/books/b",
185+
"id":2,
186+
"body":"Worst book ever!"
187+
}
188+
],
189+
"hydra:totalItems":2
190+
}
191+
"""
192+
193+
Scenario: Get a collection referencing an invalid itemUriTemplate
194+
When I send a "GET" request to "/issue5662/admin/reviews"
195+
Then the response status code should be 200
196+
And the response should be in JSON
197+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
198+
And the JSON should be equal to:
199+
"""
200+
{
201+
"@context": "/contexts/Review",
202+
"@id": "/issue5662/admin/reviews",
203+
"@type": "hydra:Collection",
204+
"hydra:totalItems": 2,
205+
"hydra:member": [
206+
{
207+
"@id": "/issue5662/admin/reviews/1",
208+
"@type": "Review",
209+
"book": "/issue5662/books/a",
210+
"id": 1,
211+
"body": "Best book ever!"
212+
},
213+
{
214+
"@id": "/issue5662/admin/reviews/2",
215+
"@type": "Review",
216+
"book": "/issue5662/books/b",
217+
"id": 2,
218+
"body": "Worst book ever!"
219+
}
220+
]
221+
}
222+
"""

features/main/sub_resource.feature

+19-5
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ Feature: Sub-resource support
564564

565565
@!mongodb
566566
@createSchema
567-
Scenario: The generated crud should allow us to interact with the SubresourceEmployee
567+
Scenario Outline: The generated crud should allow us to interact with the subresources
568568
Given I add "Content-Type" header equal to "application/ld+json"
569569
And I send a "POST" request to "/subresource_organizations" with body:
570570
"""
@@ -574,22 +574,36 @@ Feature: Sub-resource support
574574
"""
575575
Then the response status code should be 201
576576
Given I add "Content-Type" header equal to "application/ld+json"
577-
And I send a "POST" request to "/subresource_organizations/1/subresource_employees" with body:
577+
And I send a "POST" request to "<invalid_uri>" with body:
578+
"""
579+
{
580+
"name": "soyuka"
581+
}
582+
"""
583+
Then the response status code should be 404
584+
Given I add "Content-Type" header equal to "application/ld+json"
585+
And I send a "POST" request to "<collection_uri>" with body:
578586
"""
579587
{
580588
"name": "soyuka"
581589
}
582590
"""
583591
Then the response status code should be 201
584-
And I send a "GET" request to "/subresource_organizations/1/subresource_employees/1"
592+
And I send a "GET" request to "<item_uri>"
585593
Then the response status code should be 200
586-
And I send a "GET" request to "/subresource_organizations/1/subresource_employees"
594+
And I send a "GET" request to "<collection_uri>"
587595
Then the response status code should be 200
588596
Given I add "Content-Type" header equal to "application/ld+json"
589-
And I send a "PUT" request to "/subresource_organizations/1/subresource_employees/1" with body:
597+
And I send a "PUT" request to "<item_uri>" with body:
590598
"""
591599
{
592600
"name": "ok"
593601
}
594602
"""
595603
Then the response status code should be 200
604+
Given I send a "DELETE" request to "<item_uri>"
605+
Then the response status code should be 204
606+
Examples:
607+
| invalid_uri | collection_uri | item_uri |
608+
| /subresource_organizations/invalid/subresource_employees | /subresource_organizations/1/subresource_employees | /subresource_organizations/1/subresource_employees/1 |
609+
| /subresource_organizations/invalid/subresource_factories | /subresource_organizations/1/subresource_factories | /subresource_organizations/1/subresource_factories/1 |

src/Api/IdentifiersExtractor.php

+5-4
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,11 @@ private function getIdentifiersFromOperation(object $item, Operation $operation,
7474
}
7575

7676
$identifiers = [];
77-
foreach ($links ?? [] as $link) {
78-
if (1 < (is_countable($link->getIdentifiers()) ? \count($link->getIdentifiers()) : 0)) {
77+
foreach ($links ?? [] as $k => $link) {
78+
$linkIdentifiers = $link->getIdentifiers() ?? [$k];
79+
if (1 < \count($linkIdentifiers)) {
7980
$compositeIdentifiers = [];
80-
foreach ($link->getIdentifiers() as $identifier) {
81+
foreach ($linkIdentifiers as $identifier) {
8182
$compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $identifier, $link->getParameterName());
8283
}
8384

@@ -86,7 +87,7 @@ private function getIdentifiersFromOperation(object $item, Operation $operation,
8687
}
8788

8889
$parameterName = $link->getParameterName();
89-
$identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $link->getIdentifiers()[0], $parameterName, $link->getToProperty());
90+
$identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $linkIdentifiers[0], $parameterName, $link->getToProperty());
9091
}
9192

9293
return $identifiers;

src/Doctrine/Common/Filter/SearchFilterTrait.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,13 @@ protected function getIdFromValue(string $value): mixed
124124
$iriConverter = $this->getIriConverter();
125125
$item = $iriConverter->getResourceFromIri($value, ['fetch_data' => false]);
126126

127-
return $this->getPropertyAccessor()->getValue($item, 'id');
127+
if (null === $this->identifiersExtractor) {
128+
return $this->getPropertyAccessor()->getValue($item, 'id');
129+
}
130+
131+
$identifiers = $this->identifiersExtractor->getIdentifiersFromItem($item);
132+
133+
return 1 === \count($identifiers) ? array_pop($identifiers) : $identifiers;
128134
} catch (InvalidArgumentException) {
129135
// Do nothing, return the raw value
130136
}

src/Doctrine/Orm/Filter/SearchFilter.php

+36-4
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
196196
if ($metadata->hasField($field)) {
197197
if ('id' === $field) {
198198
$values = array_map($this->getIdFromValue(...), $values);
199+
// todo: handle composite IDs
199200
}
200201

201202
if (!$this->hasValidValues($values, $this->getDoctrineFieldType($property, $resourceClass))) {
@@ -216,13 +217,44 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
216217
return;
217218
}
218219

219-
$values = array_map($this->getIdFromValue(...), $values);
220-
220+
// association, let's fetch the entity (or reference to it) if we can so we can make sure we get its orm id
221221
$associationResourceClass = $metadata->getAssociationTargetClass($field);
222-
$associationFieldIdentifier = $metadata->getIdentifierFieldNames()[0];
222+
$associationMetadata = $this->getClassMetadata($associationResourceClass);
223+
$associationFieldIdentifier = $associationMetadata->getIdentifierFieldNames()[0];
223224
$doctrineTypeField = $this->getDoctrineFieldType($associationFieldIdentifier, $associationResourceClass);
224225

225-
if (!$this->hasValidValues($values, $doctrineTypeField)) {
226+
$values = array_map(function ($value) use ($associationFieldIdentifier, $doctrineTypeField) {
227+
if (is_numeric($value)) {
228+
return $value;
229+
}
230+
try {
231+
$item = $this->getIriConverter()->getResourceFromIri($value, ['fetch_data' => false]);
232+
233+
return $this->propertyAccessor->getValue($item, $associationFieldIdentifier);
234+
} catch (InvalidArgumentException) {
235+
/*
236+
* Can we do better? This is not the ApiResource the call was made on,
237+
* so we don't get any kind of api metadata for it without (a lot of?) work elsewhere...
238+
* Let's just pretend it's always the ORM id for now.
239+
*/
240+
if (!$this->hasValidValues([$value], $doctrineTypeField)) {
241+
$this->logger->notice('Invalid filter ignored', [
242+
'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $associationFieldIdentifier)),
243+
]);
244+
245+
return null;
246+
}
247+
248+
return $value;
249+
}
250+
}, $values);
251+
252+
$expected = \count($values);
253+
$values = array_filter($values, static fn ($value) => null !== $value);
254+
if ($expected > \count($values)) {
255+
/*
256+
* Shouldn't this actually fail harder?
257+
*/
226258
$this->logger->notice('Invalid filter ignored', [
227259
'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $field)),
228260
]);

src/Doctrine/Orm/State/ItemProvider.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
5252
$manager = $this->managerRegistry->getManagerForClass($entityClass);
5353

5454
$fetchData = $context['fetch_data'] ?? true;
55-
if (!$fetchData) {
55+
if (!$fetchData && \array_key_exists('id', $uriVariables)) {
56+
// todo : if uriVariables don't contain the id, this fails. This should behave like it does in the following code
5657
return $manager->getReference($entityClass, $uriVariables);
5758
}
5859

src/Hydra/Serializer/CollectionNormalizer.php

+2-20
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer
3939
self::IRI_ONLY => false,
4040
];
4141

42-
public function __construct(private readonly ContextBuilderInterface $contextBuilder, ResourceClassResolverInterface $resourceClassResolver, private readonly IriConverterInterface $iriConverter, private readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, array $defaultContext = [])
42+
public function __construct(private readonly ContextBuilderInterface $contextBuilder, ResourceClassResolverInterface $resourceClassResolver, private readonly IriConverterInterface $iriConverter, readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, array $defaultContext = [])
4343
{
4444
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
4545

46-
if ($this->resourceMetadataCollectionFactory) {
46+
if ($resourceMetadataCollectionFactory) {
4747
trigger_deprecation('api-platform/core', '3.0', sprintf('Injecting "%s" within "%s" is not needed anymore and this dependency will be removed in 4.0.', ResourceMetadataCollectionFactoryInterface::class, self::class));
4848
}
4949

@@ -80,26 +80,8 @@ protected function getItemsData(iterable $object, string $format = null, array $
8080
{
8181
$data = [];
8282
$data['hydra:member'] = [];
83-
8483
$iriOnly = $context[self::IRI_ONLY] ?? $this->defaultContext[self::IRI_ONLY];
8584

86-
if (($operation = $context['operation'] ?? null) && method_exists($operation, 'getItemUriTemplate')) {
87-
$context['item_uri_template'] = $operation->getItemUriTemplate();
88-
}
89-
90-
// We need to keep this operation for serialization groups for later
91-
if (isset($context['operation'])) {
92-
$context['root_operation'] = $context['operation'];
93-
}
94-
95-
if (isset($context['operation_name'])) {
96-
$context['root_operation_name'] = $context['operation_name'];
97-
}
98-
99-
// We need to unset the operation to ensure a proper IRI generation inside items
100-
unset($context['operation']);
101-
unset($context['operation_name'], $context['uri_variables']);
102-
10385
foreach ($object as $obj) {
10486
if ($iriOnly) {
10587
$data['hydra:member'][] = $this->iriConverter->getIriFromResource($obj);

src/JsonLd/Serializer/ItemNormalizer.php

-4
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,6 @@ public function normalize(mixed $object, string $format = null, array $context =
9999
unset($context['operation'], $context['operation_name']);
100100
}
101101

102-
if (($operation = $context['operation'] ?? null) && method_exists($operation, 'getItemUriTemplate') && ($itemUriTemplate = $operation->getItemUriTemplate())) {
103-
$context['item_uri_template'] = $itemUriTemplate;
104-
}
105-
106102
if (true === ($context['force_iri_generation'] ?? true) && $iri = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context)) {
107103
$context['iri'] = $iri;
108104
$metadata['@id'] = $iri;

src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public function create(string $resourceClass): ResourceMetadataCollection
7171
// No item operation has been found on all resources for resource class: generate one on the last resource
7272
// Helpful to generate an IRI for a resource without declaring the Get operation
7373
/** @var HttpOperation $operation */
74-
[$key, $operation] = $this->getOperationWithDefaults($resource, new NotExposed(), true, ['uriTemplate']); // @phpstan-ignore-line $resource is defined if count > 0
74+
[$key, $operation] = $this->getOperationWithDefaults(resource: $resource, operation: new NotExposed(), generated: true, ignoredOptions: ['uriTemplate', 'uriVariables']); // @phpstan-ignore-line $resource is defined if count > 0
7575

7676
if (!$this->linkFactory->createLinksFromIdentifiers($operation)) {
7777
$operation = $operation->withUriTemplate(self::$skolemUriTemplate);

src/Metadata/Tests/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactoryTest.php

+2
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ class: AttributeResource::class
208208
shortName: 'AttributeResource',
209209
types: ['https://schema.org/Book'],
210210
uriTemplate: '/custom_api_resources', // uriTemplate should not be inherited on NotExposed operation
211+
uriVariables: ['slug'], // same as it is used to generate the uriTemplate of our NotExposed operation
211212
operations: [
212213
'_api_AttributeResource_get_collection' => new GetCollection(controller: 'api_platform.action.placeholder', shortName: 'AttributeResource', class: AttributeResource::class),
213214
],
@@ -227,6 +228,7 @@ class: AttributeResource::class
227228
new ApiResource(
228229
shortName: 'AttributeResource',
229230
uriTemplate: '/custom_api_resources',
231+
uriVariables: ['slug'],
230232
types: ['https://schema.org/Book'],
231233
operations: [
232234
'_api_AttributeResource_get_collection' => new GetCollection(controller: 'api_platform.action.placeholder', shortName: 'AttributeResource', class: AttributeResource::class),

src/Serializer/AbstractCollectionNormalizer.php

+5-19
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ abstract class AbstractCollectionNormalizer implements NormalizerInterface, Norm
3434
initContext as protected;
3535
}
3636
use NormalizerAwareTrait;
37+
use OperationContextTrait;
3738

3839
/**
3940
* This constant must be overridden in the child class.
@@ -96,27 +97,12 @@ public function normalize(mixed $object, string $format = null, array $context =
9697
}
9798

9899
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class']);
99-
$context = $this->initContext($resourceClass, $context);
100+
$collectionContext = $this->initContext($resourceClass, $context);
100101
$data = [];
101-
$paginationData = $this->getPaginationData($object, $context);
102+
$paginationData = $this->getPaginationData($object, $collectionContext);
102103

103-
if (($operation = $context['operation'] ?? null) && method_exists($operation, 'getItemUriTemplate')) {
104-
$context['item_uri_template'] = $operation->getItemUriTemplate();
105-
}
106-
107-
// We need to keep this operation for serialization groups for later
108-
if (isset($context['operation'])) {
109-
$context['root_operation'] = $context['operation'];
110-
}
111-
112-
if (isset($context['operation_name'])) {
113-
$context['root_operation_name'] = $context['operation_name'];
114-
}
115-
116-
unset($context['operation']);
117-
unset($context['operation_type'], $context['operation_name']);
118-
119-
$itemsData = $this->getItemsData($object, $format, $context);
104+
$childContext = $this->createOperationContext($collectionContext, $resourceClass);
105+
$itemsData = $this->getItemsData($object, $format, $childContext);
120106

121107
return array_merge_recursive($data, $paginationData, $itemsData);
122108
}

0 commit comments

Comments
 (0)