Skip to content

Commit 432992e

Browse files
committed
Merge 4.4 into main
# Conflicts: # composer.json # src/Doctrine/Common/Filter/PropertyAwareFilterInterface.php # src/Doctrine/Odm/composer.json # src/Doctrine/Orm/composer.json # src/Elasticsearch/composer.json # src/GraphQl/composer.json # src/Hal/composer.json # src/HttpCache/composer.json # src/Hydra/composer.json # src/JsonApi/composer.json # src/JsonSchema/composer.json # src/Metadata/composer.json # src/OpenApi/composer.json # src/RamseyUuid/composer.json # src/Serializer/AbstractItemNormalizer.php # src/Serializer/composer.json # src/Symfony/composer.json # src/Validator/composer.json
2 parents ea1f385 + 2d104d0 commit 432992e

70 files changed

Lines changed: 1753 additions & 487 deletions

File tree

Some content is hidden

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

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1193,7 +1193,7 @@ jobs:
11931193
- name: Install api-platform/laravel
11941194
run: |
11951195
composer require laravel/framework:"^13.0" --no-update
1196-
composer global link ../src/Laravel --permanent
1196+
composer global link ../ --working-directory=$(pwd) --permanent
11971197
composer config minimum-stability dev
11981198
composer config prefer-stable true
11991199
composer require api-platform/laravel:"@dev" -W

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,31 @@
3838
* [c9e5071d9](https://github.com/api-platform/core/commit/c9e5071d973caedeb9b554f885dedbc89743b93d) feat(symfony): deprecate Symfony Security AccessDeniedException (#8318)
3939
* [cc0ae1254](https://github.com/api-platform/core/commit/cc0ae1254acaf9742c7f899dd24a14d46c89ca6e) feat: support dynamic HTTP response status code via request attribute (#7904)
4040

41+
## v4.3.17
42+
43+
### Bug fixes
44+
45+
* [079c7461f](https://github.com/api-platform/core/commit/079c7461f947714f15bb29b41fb143ed4bb171ea) fix(openapi): don't inherit name converter from a possibly-missing Symfony parent (#8386)
46+
* [3d48a4aa4](https://github.com/api-platform/core/commit/3d48a4aa4d87f1fa80fe19361393d645d0d013c2) fix(serializer): preserve denormalization errors for nullable object properties (#8393)
47+
* [56de59737](https://github.com/api-platform/core/commit/56de59737c53e18120ed0b9b6c40633c109d58c2) fix(doctrine): fetch_data=false reference for stateOptions resources (#8387)
48+
* [b396ff938](https://github.com/api-platform/core/commit/b396ff938272280a26d38d89011970dcee56d721) fix(serializer): report enum backing type in denormalization violations (#8389)
49+
50+
## v4.3.16
51+
52+
### Bug fixes
53+
54+
* [328cb67ce](https://github.com/api-platform/core/commit/328cb67ceda3d934d8985fb0f7e08542f59fb38d) fix(symfony): don't expose entrypoint in openapi format (#8383)
55+
* [340e982c2](https://github.com/api-platform/core/commit/340e982c22ebc6c4cf45a6f481151f668722eaf9) fix(test): remove stale phpstan ignores in CollectionNormalizerTest (#8384)
56+
* [43e3b4cf7](https://github.com/api-platform/core/commit/43e3b4cf716ed7b0d3962b5e022d065a2bb746c9) fix(openapi): don't apply the global name converter to the generated document (#8360)
57+
* [48e8f9781](https://github.com/api-platform/core/commit/48e8f97816a945d205b0cfaf4c76cc532c143312) fix(doctrine): use PHP property name in DQL for modern filters with name converter (#8382)
58+
* [66829859f](https://github.com/api-platform/core/commit/66829859fac0fe0363b8a91fd338d3ab71877774) fix(doctrine): support filtering scalar enum columns by IRI (#8358)
59+
* [71918e464](https://github.com/api-platform/core/commit/71918e4644ed2f6ce1ff8514b9ee87a57f074285) fix(jsonschema): respect readableLink for resource-typed properties on non-resource parents (#8362)
60+
* [798b4cc53](https://github.com/api-platform/core/commit/798b4cc53dee7ef1e8dd8042668bbf9f8086b746) fix(serializer): denormalize nullable collections of enums/objects (#8381)
61+
* [a9c1b538c](https://github.com/api-platform/core/commit/a9c1b538cb48ee5e629ed3909ce56653970c8889) fix: clone Parameters before mutation in security and validator providers (#8378)
62+
* [f1f33cac9](https://github.com/api-platform/core/commit/f1f33cac9a15331bc940654d37bf9fd9828d0c28) fix(ci): link monorepo root in laravel e2e so siblings resolve from working tree (#8363)
63+
* [f9d3706bb](https://github.com/api-platform/core/commit/f9d3706bb583f3a7dd74be1cc94729e49cc0611c) fix(mcp): make tools/list resilient to an empty registry (#8371)
64+
* [fe84af236](https://github.com/api-platform/core/commit/fe84af2365244055737df73f888f7aa3888f462e) fix(symfony): allow null $data in PlaceholderAction (#8359)
65+
4166
## v4.3.15
4267

4368
### Bug fixes

composer.json

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"dev-main": "5.0.x-dev"
5454
},
5555
"symfony": {
56-
"require": "^6.4 || ^7.1 || ^8.0"
56+
"require": "^7.4 || ^8.0"
5757
},
5858
"pmu": {
5959
"projects": [
@@ -113,15 +113,15 @@
113113
"psr/cache": "^1.0 || ^2.0 || ^3.0",
114114
"psr/container": "^1.0 || ^2.0",
115115
"symfony/deprecation-contracts": "^3.1",
116-
"symfony/http-foundation": "^6.4.14 || ^7.0 || ^8.0",
117-
"symfony/http-kernel": "^6.4.13 || ^7.0 || ^8.0",
118-
"symfony/property-access": "^6.4 || ^7.0 || ^8.0",
119-
"symfony/property-info": "^7.1 || ^8.0",
120-
"symfony/serializer": "^6.4.37 || ^7.4.9 || ^8.0.9",
116+
"symfony/http-foundation": "^7.4 || ^8.0",
117+
"symfony/http-kernel": "^7.4 || ^8.0",
118+
"symfony/property-access": "^7.4 || ^8.0",
119+
"symfony/property-info": "^7.4 || ^8.0",
120+
"symfony/serializer": "^7.4.9 || ^8.0.9",
121121
"symfony/translation-contracts": "^3.3",
122122
"symfony/type-info": "^7.4 || ^8.0",
123-
"symfony/validator": "^6.4.11 || ^7.1 || ^8.0",
124-
"symfony/web-link": "^6.4 || ^7.1 || ^8.0",
123+
"symfony/validator": "^7.4 || ^8.0",
124+
"symfony/web-link": "^7.4 || ^8.0",
125125
"willdurand/negotiation": "^3.1"
126126
},
127127
"require-dev": {
@@ -157,39 +157,39 @@
157157
"ramsey/uuid-doctrine": "^2.0",
158158
"soyuka/pmu": "^0.2.0",
159159
"soyuka/stubs-mongodb": "^1.0",
160-
"symfony/asset": "^6.4 || ^7.0 || ^8.0",
161-
"symfony/browser-kit": "^6.4 || ^7.0 || ^8.0",
162-
"symfony/cache": "^6.4 || ^7.0 || ^8.0",
163-
"symfony/config": "^6.4 || ^7.0 || ^8.0",
164-
"symfony/console": "^6.4 || ^7.0 || ^8.0",
165-
"symfony/css-selector": "^6.4 || ^7.0 || ^8.0",
166-
"symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0",
167-
"symfony/doctrine-bridge": "^6.4.2 || ^7.1 || ^8.0",
168-
"symfony/dom-crawler": "^6.4 || ^7.0 || ^8.0",
169-
"symfony/error-handler": "^6.4 || ^7.0 || ^8.0",
170-
"symfony/event-dispatcher": "^6.4 || ^7.0 || ^8.0",
171-
"symfony/expression-language": "^6.4 || ^7.0 || ^8.0",
172-
"symfony/finder": "^6.4 || ^7.0 || ^8.0",
173-
"symfony/form": "^6.4 || ^7.0 || ^8.0",
174-
"symfony/framework-bundle": "^6.4 || ^7.0 || ^8.0",
175-
"symfony/http-client": "^6.4 || ^7.0 || ^8.0",
176-
"symfony/intl": "^6.4 || ^7.0 || ^8.0",
160+
"symfony/asset": "^7.4 || ^8.0",
161+
"symfony/browser-kit": "^7.4 || ^8.0",
162+
"symfony/cache": "^7.4 || ^8.0",
163+
"symfony/config": "^7.4 || ^8.0",
164+
"symfony/console": "^7.4 || ^8.0",
165+
"symfony/css-selector": "^7.4 || ^8.0",
166+
"symfony/dependency-injection": "^7.4 || ^8.0",
167+
"symfony/doctrine-bridge": "^7.4 || ^8.0",
168+
"symfony/dom-crawler": "^7.4 || ^8.0",
169+
"symfony/error-handler": "^7.4 || ^8.0",
170+
"symfony/event-dispatcher": "^7.4 || ^8.0",
171+
"symfony/expression-language": "^7.4 || ^8.0",
172+
"symfony/finder": "^7.4 || ^8.0",
173+
"symfony/form": "^7.4 || ^8.0",
174+
"symfony/framework-bundle": "^7.4 || ^8.0",
175+
"symfony/http-client": "^7.4 || ^8.0",
176+
"symfony/intl": "^7.4 || ^8.0",
177177
"symfony/json-streamer": "^7.4 || ^8.0",
178178
"symfony/maker-bundle": "^1.24",
179179
"symfony/mcp-bundle": "dev-main",
180180
"symfony/mercure-bundle": "*",
181-
"symfony/messenger": "^6.4 || ^7.0 || ^8.0",
181+
"symfony/messenger": "^7.4 || ^8.0",
182182
"symfony/object-mapper": "^7.4 || ^8.0",
183-
"symfony/routing": "^6.4 || ^7.0 || ^8.0",
184-
"symfony/security-bundle": "^6.4 || ^7.0 || ^8.0",
185-
"symfony/security-core": "^6.4 || ^7.0 || ^8.0",
186-
"symfony/stopwatch": "^6.4 || ^7.0 || ^8.0",
187-
"symfony/string": "^6.4 || ^7.0 || ^8.0",
188-
"symfony/twig-bundle": "^6.4 || ^7.0 || ^8.0",
189-
"symfony/uid": "^6.4 || ^7.0 || ^8.0",
183+
"symfony/routing": "^7.4 || ^8.0",
184+
"symfony/security-bundle": "^7.4 || ^8.0",
185+
"symfony/security-core": "^7.4 || ^8.0",
186+
"symfony/stopwatch": "^7.4 || ^8.0",
187+
"symfony/string": "^7.4 || ^8.0",
188+
"symfony/twig-bundle": "^7.4 || ^8.0",
189+
"symfony/uid": "^7.4 || ^8.0",
190190
"symfony/var-exporter": "^7.4 || ^8.0",
191-
"symfony/web-profiler-bundle": "^6.4 || ^7.0 || ^8.0",
192-
"symfony/yaml": "^6.4 || ^7.0 || ^8.0",
191+
"symfony/web-profiler-bundle": "^7.4 || ^8.0",
192+
"symfony/yaml": "^7.4 || ^8.0",
193193
"twig/twig": "^1.42.3 || ^2.12 || ^3.0",
194194
"webonyx/graphql-php": "^15.0"
195195
},

src/Doctrine/Common/composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"doctrine/orm": "^2.17 || ^3.0",
3636
"phpspec/prophecy-phpunit": "^2.2",
3737
"phpunit/phpunit": "^11.5 || ^12.2",
38-
"symfony/type-info": "^7.3 || ^8.0"
38+
"symfony/type-info": "^7.4 || ^8.0"
3939
},
4040
"conflict": {
4141
"doctrine/persistence": "<1.3"
@@ -67,7 +67,7 @@
6767
"dev-4.1": "4.1.x-dev"
6868
},
6969
"symfony": {
70-
"require": "^6.4 || ^7.0 || ^8.0"
70+
"require": "^7.4 || ^8.0"
7171
},
7272
"thanks": {
7373
"name": "api-platform/api-platform",

src/Doctrine/Odm/Filter/IriFilter.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use ApiPlatform\State\ParameterProvider\IriConverterParameterProvider;
2626
use Doctrine\ODM\MongoDB\Aggregation\Builder;
2727
use Doctrine\ODM\MongoDB\DocumentManager;
28+
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
2829
use Doctrine\ODM\MongoDB\Mapping\MappingException;
2930

3031
/**
@@ -68,7 +69,23 @@ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Opera
6869
$leafProperty = $nestedInfo['leaf_property'] ?? $property;
6970
$classMetadata = $documentManager->getClassMetadata($leafClass);
7071

72+
// Backed enum exposed as a resource: match the scalar field against the resolved enum.
7173
if (!$classMetadata->hasReference($leafProperty)) {
74+
if (!$this->isEnumField($classMetadata, $leafProperty)) {
75+
return;
76+
}
77+
78+
$normalize = static fn (mixed $v): mixed => $v instanceof \BackedEnum ? $v->value : $v;
79+
80+
if (is_iterable($value)) {
81+
$values = \is_array($value) ? $value : iterator_to_array($value);
82+
$match->{$operator}($aggregationBuilder->matchExpr()->field($matchField)->in(array_map($normalize, $values)));
83+
84+
return;
85+
}
86+
87+
$match->{$operator}($aggregationBuilder->matchExpr()->field($matchField)->equals($normalize($value)));
88+
7289
return;
7390
}
7491

@@ -113,4 +130,9 @@ public static function getParameterProvider(): string
113130
{
114131
return IriConverterParameterProvider::class;
115132
}
133+
134+
private function isEnumField(ClassMetadata $classMetadata, string $field): bool
135+
{
136+
return $classMetadata->hasField($field) && null !== ($classMetadata->getFieldMapping($field)['enumType'] ?? null);
137+
}
116138
}

src/Doctrine/Odm/NestedPropertyHelperTrait.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ protected function addNestedParameterLookups(string $property, Builder $aggregat
3939
$nestedInfo = ($extraProperties['nested_properties_info'] ?? []) ? reset($extraProperties['nested_properties_info']) : null;
4040

4141
if (!$nestedInfo) {
42-
return $property;
42+
return $extraProperties['query_property'] ?? $property;
4343
}
4444

4545
$odmSegments = $nestedInfo['odm_segments'] ?? [];

src/Doctrine/Odm/composer.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,21 @@
3030
"api-platform/serializer": "^5.0@alpha",
3131
"api-platform/state": "^5.0@alpha",
3232
"doctrine/mongodb-odm": "^2.10",
33-
"symfony/property-info": "^7.1 || ^8.0",
34-
"symfony/type-info": "^7.3 || ^8.0"
33+
"symfony/property-info": "^7.4 || ^8.0",
34+
"symfony/type-info": "^7.4 || ^8.0"
3535
},
3636
"require-dev": {
3737
"doctrine/doctrine-bundle": "^2.11 || ^3.1",
3838
"doctrine/mongodb-odm-bundle": "^5.0",
3939
"phpspec/prophecy-phpunit": "^2.2",
4040
"phpunit/phpunit": "^11.5 || ^12.2",
41-
"symfony/cache": "^6.4 || ^7.0 || ^8.0",
42-
"symfony/framework-bundle": "^6.4 || ^7.0 || ^8.0",
43-
"symfony/property-access": "^6.4 || ^7.0 || ^8.0",
44-
"symfony/serializer": "^6.4 || ^7.0 || ^8.0",
45-
"symfony/uid": "^6.4 || ^7.0 || ^8.0",
46-
"symfony/validator": "^6.4.11 || ^7.0 || ^8.0",
47-
"symfony/yaml": "^6.4 || ^7.0 || ^8.0"
41+
"symfony/cache": "^7.4 || ^8.0",
42+
"symfony/framework-bundle": "^7.4 || ^8.0",
43+
"symfony/property-access": "^7.4 || ^8.0",
44+
"symfony/serializer": "^7.4 || ^8.0",
45+
"symfony/uid": "^7.4 || ^8.0",
46+
"symfony/validator": "^7.4 || ^8.0",
47+
"symfony/yaml": "^7.4 || ^8.0"
4848
},
4949
"autoload": {
5050
"psr-4": {
@@ -68,7 +68,7 @@
6868
"dev-4.1": "4.1.x-dev"
6969
},
7070
"symfony": {
71-
"require": "^6.4 || ^7.0 || ^8.0"
71+
"require": "^7.4 || ^8.0"
7272
},
7373
"thanks": {
7474
"name": "api-platform/api-platform",

src/Doctrine/Orm/Filter/IriFilter.php

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
use ApiPlatform\Metadata\Operation;
2323
use ApiPlatform\Metadata\ParameterProviderFilterInterface;
2424
use ApiPlatform\State\ParameterProvider\IriConverterParameterProvider;
25+
use Doctrine\ORM\Mapping\ClassMetadata;
26+
use Doctrine\ORM\Mapping\FieldMapping;
2527
use Doctrine\ORM\QueryBuilder;
2628

2729
/**
@@ -57,6 +59,13 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q
5759
$ormLeafMetadata = $this->resolveLeafMetadataAtRuntime($queryBuilder, $resourceClass, $parameter->getProperty(), $property);
5860
}
5961

62+
// Backed enum exposed as a resource: compare the scalar column to the resolved enum.
63+
if (null === $ormLeafMetadata) {
64+
$this->applyEnumComparison($queryBuilder, $alias, $property, $parameterName, $value, $context);
65+
66+
return;
67+
}
68+
6069
// Collection associations (OneToMany/ManyToMany) require a JOIN to compare individual elements.
6170
if ($ormLeafMetadata['is_collection_valued']) {
6271
$queryBuilder->join(\sprintf('%s.%s', $alias, $property), $parameterName);
@@ -102,6 +111,25 @@ public static function getParameterProvider(): string
102111
return IriConverterParameterProvider::class;
103112
}
104113

114+
private function applyEnumComparison(QueryBuilder $queryBuilder, string $alias, string $property, string $parameterName, mixed $value, array $context): void
115+
{
116+
$propertyExpr = \sprintf('%s.%s', $alias, $property);
117+
$normalize = static fn (mixed $v): mixed => $v instanceof \BackedEnum ? $v->value : $v;
118+
119+
if (is_iterable($value)) {
120+
$values = \is_array($value) ? $value : iterator_to_array($value);
121+
$queryBuilder
122+
->{$context['whereClause'] ?? 'andWhere'}(\sprintf('%s IN (:%s)', $propertyExpr, $parameterName))
123+
->setParameter($parameterName, array_map($normalize, $values));
124+
125+
return;
126+
}
127+
128+
$queryBuilder
129+
->{$context['whereClause'] ?? 'andWhere'}(\sprintf('%s = :%s', $propertyExpr, $parameterName))
130+
->setParameter($parameterName, $normalize($value));
131+
}
132+
105133
/**
106134
* @return array{is_collection_valued: bool, association_target_class: string, identifier_type: ?string}|null
107135
*/
@@ -122,9 +150,12 @@ private function getOrmLeafMetadata(mixed $parameter): ?array
122150
* Resolves leaf metadata at runtime by walking the association chain.
123151
* Used as fallback when precomputed orm_leaf_metadata is not available.
124152
*
125-
* @return array{is_collection_valued: bool, association_target_class: string, identifier_type: ?string}
153+
* Returns null when the leaf is a backed enum column so the caller compares it directly;
154+
* a non-association, non-enum leaf is left to raise a mapping error.
155+
*
156+
* @return array{is_collection_valued: bool, association_target_class: string, identifier_type: ?string}|null
126157
*/
127-
private function resolveLeafMetadataAtRuntime(QueryBuilder $queryBuilder, string $resourceClass, string $originalProperty, string $leafProperty): array
158+
private function resolveLeafMetadataAtRuntime(QueryBuilder $queryBuilder, string $resourceClass, string $originalProperty, string $leafProperty): ?array
128159
{
129160
$em = $queryBuilder->getEntityManager();
130161
$metadata = $em->getClassMetadata($resourceClass);
@@ -135,6 +166,10 @@ private function resolveLeafMetadataAtRuntime(QueryBuilder $queryBuilder, string
135166
$metadata = $em->getClassMetadata($associationMapping['targetEntity']);
136167
}
137168

169+
if (!$metadata->hasAssociation($leafProperty) && $this->isEnumField($metadata, $leafProperty)) {
170+
return null;
171+
}
172+
138173
$isCollectionValued = $metadata->isCollectionValuedAssociation($leafProperty);
139174
$associationMapping = $metadata->getAssociationMapping($leafProperty);
140175
$targetClass = $associationMapping['targetEntity'];
@@ -152,4 +187,19 @@ private function resolveLeafMetadataAtRuntime(QueryBuilder $queryBuilder, string
152187
'identifier_type' => $identifierType,
153188
];
154189
}
190+
191+
private function isEnumField(ClassMetadata $metadata, string $field): bool
192+
{
193+
if (!isset($metadata->fieldMappings[$field])) {
194+
return false;
195+
}
196+
197+
$fieldMapping = $metadata->fieldMappings[$field];
198+
// Doctrine ORM 2.x returns an array and Doctrine ORM 3.x returns a FieldMapping object.
199+
if ($fieldMapping instanceof FieldMapping) {
200+
$fieldMapping = (array) $fieldMapping;
201+
}
202+
203+
return null !== ($fieldMapping['enumType'] ?? null);
204+
}
155205
}

src/Doctrine/Orm/NestedPropertyHelperTrait.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ protected function addNestedParameterJoins(
4343
$nestedInfo = ($extraProperties['nested_properties_info'] ?? []) ? reset($extraProperties['nested_properties_info']) : null;
4444

4545
if (!$nestedInfo) {
46-
return [$alias, $property];
46+
return [$alias, $extraProperties['query_property'] ?? $property];
4747
}
4848

4949
$relationClasses = $nestedInfo['relation_classes'] ?? [];

src/Doctrine/Orm/State/ItemProvider.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,10 @@ private function getReferenceIdentifiers(EntityManagerInterface $manager, string
124124

125125
$identifiers = [];
126126
foreach ($this->getLinks($entityClass, $operation, $context) as $parameterName => $link) {
127-
// Mirrors LinksHandlerTrait: the identifier-self link has no relation property and points to the entity itself.
128-
if ($entityClass !== $link->getFromClass() || $link->getFromProperty() || $link->getToProperty()) {
127+
// Mirrors LinksHandlerTrait: the identifier-self link has no relation property and points to the resource itself.
128+
// Links are expressed in resource terms, so the identifier-self link's fromClass is the resource class
129+
// ($operation->getClass()), which differs from $entityClass for a DTO/stateOptions resource.
130+
if ($operation->getClass() !== $link->getFromClass() || $link->getFromProperty() || $link->getToProperty()) {
129131
continue;
130132
}
131133

0 commit comments

Comments
 (0)