Skip to content

Commit c58850b

Browse files
committed
Merge 3.1
2 parents 92a81f0 + 3e3595d commit c58850b

File tree

19 files changed

+152
-393
lines changed

19 files changed

+152
-393
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## v3.1.14
4+
5+
### Bug fixes
6+
7+
* [146f55330](https://github.com/api-platform/core/commit/146f55330e3df8301ac84345b69a25cdfb908b27) fix(metadata): operation NotExposed status to 404 (#5717)
8+
* [4dcfc16c3](https://github.com/api-platform/core/commit/4dcfc16c38ab4c371a37a7d92d2f2f205de31f89) fix(symfony): perf regression with Symfony 6.3 (#5721)
9+
* [4f9626f42](https://github.com/api-platform/core/commit/4f9626f42b75a5fd1f9d681c80ad6c4ee56318fe) fix(serializer): use data if no uri_variables provided (#5743)
10+
* [7bb92a52f](https://github.com/api-platform/core/commit/7bb92a52f5c6e02705547408281eba93f73b588e) fix(doctrine): use stateOptions only within doctrine context (#5726)
11+
* [83dbfbff1](https://github.com/api-platform/core/commit/83dbfbff1717dabba7ce9e814d0bdb556b49fcb8) fix(metadata): generated NotExposed operation should inherit resource options (#5722)
12+
* [ccad63683](https://github.com/api-platform/core/commit/ccad6368303d341f37eff0317cc8e433504c460f) Revert "fix: search on nested sub-entity that doesn't use "id" as its ORM identifier (#5623)" (#5744)
13+
* [e2745855b](https://github.com/api-platform/core/commit/e2745855be4986d361626d1b853e45cde229d3d8) fix(openapi): model Example, Header and Reference (#5716)
14+
* [ebf03104f](https://github.com/api-platform/core/commit/ebf03104fcbffc5af74d78c3e9b14d02d7527214) fix(jsonld): skolem uri template may have a _format (#5729)
15+
316
## v3.1.13
417

518
### Bug fixes

features/doctrine/search_filter.feature

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,15 +1025,6 @@ Feature: Search filter on collections
10251025
And the response should be in JSON
10261026
And the JSON node "hydra:totalItems" should be equal to 1
10271027

1028-
@!mongodb
1029-
@createSchema
1030-
Scenario: Search on nested sub-entity that doesn't use "id" as its ORM identifier
1031-
Given there is a dummy entity with a sub entity with id "stringId" and name "someName"
1032-
When I send a "GET" request to "/dummy_with_subresource?subEntity=/dummy_subresource/stringId"
1033-
Then the response status code should be 200
1034-
And the response should be in JSON
1035-
And the JSON node "hydra:totalItems" should be equal to 1
1036-
10371028
@!mongodb
10381029
@createSchema
10391030
Scenario: Custom search filters can use Doctrine Expressions as join conditions

features/main/patch.feature

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,25 @@ Feature: Sending PATCH requets
5858
}
5959
}
6060
"""
61+
62+
Scenario: Patch a relation with uri variables that are not `id`
63+
When I add "Content-Type" header equal to "application/merge-patch+json"
64+
And I send a "PATCH" request to "/betas/1" with body:
65+
"""
66+
{
67+
"alpha": "/alphas/2"
68+
}
69+
"""
70+
Then the response should be in JSON
71+
And the response status code should be 200
72+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
73+
And the JSON should be equal to:
74+
"""
75+
{
76+
"@context": "/contexts/Beta",
77+
"@id": "/betas/1",
78+
"@type": "Beta",
79+
"betaId": 1,
80+
"alpha": "/alphas/2"
81+
}
82+
"""

src/Doctrine/Common/Filter/SearchFilterTrait.php

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

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;
127+
return $this->getPropertyAccessor()->getValue($item, 'id');
134128
} catch (InvalidArgumentException) {
135129
// Do nothing, return the raw value
136130
}

src/Doctrine/Orm/Filter/SearchFilter.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,6 @@ 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
200199
}
201200

202201
if (!$this->hasValidValues($values, $this->getDoctrineFieldType($property, $resourceClass))) {
@@ -218,11 +217,9 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
218217
}
219218

220219
$values = array_map($this->getIdFromValue(...), $values);
221-
// todo: handle composite IDs
222220

223221
$associationResourceClass = $metadata->getAssociationTargetClass($field);
224-
$associationMetadata = $this->getClassMetadata($associationResourceClass);
225-
$associationFieldIdentifier = $associationMetadata->getIdentifierFieldNames()[0];
222+
$associationFieldIdentifier = $metadata->getIdentifierFieldNames()[0];
226223
$doctrineTypeField = $this->getDoctrineFieldType($associationFieldIdentifier, $associationResourceClass);
227224

228225
if (!$this->hasValidValues($values, $doctrineTypeField)) {

src/Serializer/AbstractItemNormalizer.php

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ApiPlatform\Exception\ItemNotFoundException;
2121
use ApiPlatform\Metadata\ApiProperty;
2222
use ApiPlatform\Metadata\CollectionOperationInterface;
23+
use ApiPlatform\Metadata\Exception\OperationNotFoundException;
2324
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2425
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2526
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
@@ -355,12 +356,12 @@ protected function getClassDiscriminatorResolvedClass(array $data, string $class
355356
}
356357

357358
if (!isset($data[$mapping->getTypeProperty()])) {
358-
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty());
359+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'] . '.' . $mapping->getTypeProperty() : $mapping->getTypeProperty());
359360
}
360361

361362
$type = $data[$mapping->getTypeProperty()];
362363
if (null === ($mappedClass = $mapping->getClassForType($type))) {
363-
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true);
364+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'] . '.' . $mapping->getTypeProperty() : $mapping->getTypeProperty(), true);
364365
}
365366

366367
return $mappedClass;
@@ -402,8 +403,7 @@ protected function getAllowedAttributes(string|object $classOrObject, array $con
402403

403404
if (
404405
$this->isAllowedAttribute($classOrObject, $propertyName, null, $context)
405-
&& (
406-
isset($context['api_normalize']) && $propertyMetadata->isReadable()
406+
&& (isset($context['api_normalize']) && $propertyMetadata->isReadable()
407407
|| isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !\is_object($classOrObject) && $propertyMetadata->isInitializable())
408408
)
409409
) {
@@ -491,7 +491,7 @@ protected function validateType(string $attribute, Type $type, mixed $value, str
491491
if (Type::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && str_contains($format, 'json')) {
492492
$isValid = \is_float($value) || \is_int($value);
493493
} else {
494-
$isValid = \call_user_func('is_'.$builtinType, $value);
494+
$isValid = \call_user_func('is_' . $builtinType, $value);
495495
}
496496

497497
if (!$isValid) {
@@ -512,15 +512,10 @@ protected function denormalizeCollection(string $attribute, ApiProperty $propert
512512

513513
$collectionKeyType = $type->getCollectionKeyTypes()[0] ?? null;
514514
$collectionKeyBuiltinType = $collectionKeyType?->getBuiltinType();
515-
$childContext = $this->createChildContext(['resource_class' => $className] + $context, $attribute, $format);
516-
unset($childContext['uri_variables']);
517-
if ($this->resourceMetadataCollectionFactory) {
518-
$childContext['operation'] = $this->resourceMetadataCollectionFactory->create($className)->getOperation();
519-
}
520-
515+
$childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
521516
$values = [];
522517
foreach ($value as $index => $obj) {
523-
if (null !== $collectionKeyBuiltinType && !\call_user_func('is_'.$collectionKeyBuiltinType, $index)) {
518+
if (null !== $collectionKeyBuiltinType && !\call_user_func('is_' . $collectionKeyBuiltinType, $index)) {
524519
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the key "%s" must be "%s", "%s" given.', $index, $collectionKeyBuiltinType, \gettype($index)), $index, [$collectionKeyBuiltinType], ($context['deserialization_path'] ?? false) ? sprintf('key(%s)', $context['deserialization_path']) : null, true);
525520
}
526521

@@ -582,7 +577,7 @@ protected function getFactoryOptions(array $context): array
582577
$options['serializer_groups'] = (array) $context[self::GROUPS];
583578
}
584579

585-
$operationCacheKey = ($context['resource_class'] ?? '').($context['operation_name'] ?? '').($context['api_normalize'] ?? '');
580+
$operationCacheKey = ($context['resource_class'] ?? '') . ($context['operation_name'] ?? '') . ($context['api_normalize'] ?? '');
586581
if ($operationCacheKey && isset($this->localFactoryOptionsCache[$operationCacheKey])) {
587582
return $options + $this->localFactoryOptionsCache[$operationCacheKey];
588583
}
@@ -637,9 +632,7 @@ protected function getAttributeValue(object $object, string $attribute, string $
637632
}
638633

639634
$resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
640-
$childContext = $this->createChildContext($context, $attribute, $format);
641-
unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['operation']);
642-
635+
$childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format);
643636
return $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
644637
}
645638

@@ -652,13 +645,7 @@ protected function getAttributeValue(object $object, string $attribute, string $
652645
}
653646

654647
$resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
655-
$childContext = $this->createChildContext($context, $attribute, $format);
656-
$childContext['resource_class'] = $resourceClass;
657-
if ($this->resourceMetadataCollectionFactory) {
658-
$childContext['operation'] = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation();
659-
}
660-
unset($childContext['iri'], $childContext['uri_variables']);
661-
648+
$childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format);
662649
return $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
663650
}
664651

@@ -671,18 +658,15 @@ protected function getAttributeValue(object $object, string $attribute, string $
671658
$context['force_resource_class'],
672659
);
673660

661+
// Anonymous resources
674662
if ($type->getClassName()) {
675-
$childContext = $this->createChildContext($context, $attribute, $format);
676-
unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['force_resource_class']);
663+
$childContext = $this->createChildContext($this->createOperationContext($context, null), $attribute, $format);
677664
$childContext['output']['gen_id'] = $propertyMetadata->getGenId() ?? true;
678-
679665
return $this->serializer->normalize($attributeValue, $format, $childContext);
680666
}
681667

682668
if ('array' === $type->getBuiltinType()) {
683-
$childContext = $this->createChildContext($context, $attribute, $format);
684-
unset($childContext['iri'], $childContext['uri_variables']);
685-
669+
$childContext = $this->createChildContext($this->createOperationContext($context, null), $attribute, $format);
686670
return $this->serializer->normalize($attributeValue, $format, $childContext);
687671
}
688672
}
@@ -811,12 +795,7 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value
811795
&& $this->resourceClassResolver->isResourceClass($className)
812796
) {
813797
$resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
814-
$childContext = $this->createChildContext($context, $attribute, $format);
815-
$childContext['resource_class'] = $resourceClass;
816-
if ($this->resourceMetadataCollectionFactory) {
817-
$childContext['operation'] = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation();
818-
}
819-
798+
$childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format);
820799
return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext);
821800
}
822801

@@ -832,7 +811,7 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value
832811

833812
unset($context['resource_class']);
834813

835-
return $this->serializer->denormalize($value, $className.'[]', $format, $context);
814+
return $this->serializer->denormalize($value, $className . '[]', $format, $context);
836815
}
837816

838817
if (null !== $className = $type->getClassName()) {
@@ -932,4 +911,29 @@ private function setValue(object $object, string $attributeName, mixed $value):
932911
// Properties not found are ignored
933912
}
934913
}
914+
915+
private function createOperationContext(array $context, string $resourceClass = null): array
916+
{
917+
if (isset($context['operation']) && !isset($context['root_operation'])) {
918+
$context['root_operation'] = $context['operation'];
919+
$context['root_operation_name'] = $context['operation_name'];
920+
}
921+
922+
unset($context['iri'], $context['uri_variables']);
923+
if (!$resourceClass) {
924+
return $context;
925+
}
926+
927+
unset($context['operation'], $context['operation_name']);
928+
$context['resource_class'] = $resourceClass;
929+
if ($this->resourceMetadataCollectionFactory) {
930+
try {
931+
$context['operation'] = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation();
932+
$context['operation_name'] = $context['operation']->getName();
933+
} catch (OperationNotFoundException) {
934+
}
935+
}
936+
937+
return $context;
938+
}
935939
}

src/Serializer/ItemNormalizer.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,7 @@ private function updateObjectToPopulate(array $data, array &$context): void
8686

8787
private function getContextUriVariables(array $data, $operation, array $context): array
8888
{
89-
if (!isset($context['uri_variables'])) {
90-
return ['id' => $data['id']];
91-
}
92-
93-
$uriVariables = $context['uri_variables'];
89+
$uriVariables = $context['uri_variables'] ?? $data;
9490

9591
/** @var Link $uriVariable */
9692
foreach ($operation->getUriVariables() as $uriVariable) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@
116116
<service id="api_platform.doctrine_mongodb.odm.search_filter" class="ApiPlatform\Doctrine\Odm\Filter\SearchFilter" public="false" abstract="true">
117117
<argument type="service" id="doctrine_mongodb" />
118118
<argument type="service" id="api_platform.iri_converter" />
119-
<argument type="service" id="api_platform.identifiers_extractor" on-invalid="ignore" />
119+
<argument type="service" id="api_platform.identifiers_extractor.cached" on-invalid="ignore" />
120120
<argument type="service" id="api_platform.property_accessor" />
121121
<argument type="service" id="logger" on-invalid="ignore" />
122122
<argument key="$nameConverter" type="service" id="api_platform.name_converter" on-invalid="ignore"/>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@
152152
<argument type="service" id="api_platform.iri_converter" />
153153
<argument type="service" id="api_platform.property_accessor" />
154154
<argument type="service" id="logger" on-invalid="ignore" />
155-
<argument key="$identifiersExtractor" type="service" id="api_platform.identifiers_extractor" on-invalid="ignore" />
155+
<argument key="$identifiersExtractor" type="service" id="api_platform.identifiers_extractor.cached" on-invalid="ignore" />
156156
<argument key="$nameConverter" type="service" id="api_platform.name_converter" on-invalid="ignore" />
157157
</service>
158158
<service id="ApiPlatform\Doctrine\Orm\Filter\SearchFilter" alias="api_platform.doctrine.orm.search_filter" />

tests/Behat/DoctrineContext.php

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,8 @@
128128
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyPassenger;
129129
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyProduct;
130130
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyProperty;
131-
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummySubEntity;
132131
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceNotApiResourceChild;
133132
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTravel;
134-
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyWithSubEntity;
135133
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EmbeddableDummy;
136134
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EmbeddedDummy;
137135
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EntityClassWithDateTime;
@@ -2145,20 +2143,6 @@ public function thereIsAResourceUsingEntityClassAndDateTime(): void
21452143
$this->manager->flush();
21462144
}
21472145

2148-
/**
2149-
* @Given there is a dummy entity with a sub entity with id :strId and name :name
2150-
*/
2151-
public function thereIsADummyWithSubEntity(string $strId, string $name): void
2152-
{
2153-
$subEntity = new DummySubEntity($strId, $name);
2154-
$mainEntity = new DummyWithSubEntity();
2155-
$mainEntity->setSubEntity($subEntity);
2156-
$mainEntity->setName('main');
2157-
$this->manager->persist($subEntity);
2158-
$this->manager->persist($mainEntity);
2159-
$this->manager->flush();
2160-
}
2161-
21622146
private function isOrm(): bool
21632147
{
21642148
return null !== $this->schemaTool;

0 commit comments

Comments
 (0)