Skip to content

Commit 07d0ef8

Browse files
committed
Merge 4.2
2 parents 8c27606 + 5e45261 commit 07d0ef8

File tree

60 files changed

+1212
-237
lines changed

Some content is hidden

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

60 files changed

+1212
-237
lines changed

features/filter/filter_validation.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Feature: Validate filters based upon filter description
1616
Scenario: Required filter should throw an error if not set
1717
When I am on "/filter_validators"
1818
Then the response status code should be 422
19-
And the JSON node "detail" should be equal to 'required: This value should not be blank.\nrequired-allow-empty: The parameter "required-allow-empty" is required.'
19+
And the JSON node "detail" should be equal to 'required: This value should not be blank.\nrequired-allow-empty: This value should not be null.'
2020

2121
Scenario: Required filter should not throw an error if set
2222
When I am on "/array_filter_validators?arrayRequired[]=foo&indexedArrayRequired[foo]=foo"

features/http_cache/headers.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ Feature: Default values of HTTP cache headers
99
Then the response status code should be 200
1010
And the header "Etag" should be equal to '"032297ac74d75a50"'
1111
And the header "Cache-Control" should be equal to "max-age=60, public, s-maxage=3600"
12-
And the header "Vary" should be equal to "Accept, Cookie"
12+
And the header "Vary" should be equal to "Accept, Cookie, Accept-Language"

src/Doctrine/Orm/Extension/ParameterExtension.php

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

1616
use ApiPlatform\Doctrine\Common\Filter\LoggerAwareInterface;
1717
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface;
18+
use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface;
1819
use ApiPlatform\Doctrine\Common\ParameterValueExtractorTrait;
1920
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
2021
use ApiPlatform\Doctrine\Orm\Filter\FilterInterface;
@@ -75,19 +76,21 @@ private function applyFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInter
7576
$filter->setLogger($this->logger);
7677
}
7778

78-
if ($filter instanceof AbstractFilter && !$filter->getProperties()) {
79+
if ($filter instanceof PropertyAwareFilterInterface) {
80+
$properties = [];
7981
$propertyKey = $parameter->getProperty() ?? $parameter->getKey();
80-
81-
if (str_contains($propertyKey, ':property')) {
82-
$extraProperties = $parameter->getExtraProperties()['_properties'] ?? [];
83-
foreach (array_keys($extraProperties) as $property) {
84-
$properties[$property] = $parameter->getFilterContext();
82+
if ($filter instanceof AbstractFilter) {
83+
$properties = $filter->getProperties() ?? [];
84+
85+
if (str_contains($propertyKey, ':property')) {
86+
$extraProperties = $parameter->getExtraProperties()['_properties'] ?? [];
87+
foreach (array_keys($extraProperties) as $property) {
88+
$properties[$property] = $parameter->getFilterContext();
89+
}
8590
}
86-
} else {
87-
$properties = [$propertyKey => $parameter->getFilterContext()];
8891
}
8992

90-
$filter->setProperties($properties ?? []);
93+
$filter->setProperties($properties + [$propertyKey => $parameter->getFilterContext()]);
9194
}
9295

9396
$filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation,

src/GraphQl/Type/FieldsBuilder.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use ApiPlatform\Metadata\Util\PropertyInfoToTypeInfoHelper;
3232
use ApiPlatform\Metadata\Util\TypeHelper;
3333
use ApiPlatform\State\Pagination\Pagination;
34+
use ApiPlatform\State\Util\StateOptionsTrait;
3435
use GraphQL\Type\Definition\InputObjectType;
3536
use GraphQL\Type\Definition\ListOfType;
3637
use GraphQL\Type\Definition\NonNull;
@@ -55,6 +56,8 @@
5556
*/
5657
final class FieldsBuilder implements FieldsBuilderEnumInterface
5758
{
59+
use StateOptionsTrait;
60+
5861
private readonly ContextAwareTypeBuilderInterface $typeBuilder;
5962

6063
public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, ContextAwareTypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $resolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?InflectorInterface $inflector = new Inflector())
@@ -85,7 +88,7 @@ public function getItemQueryFields(string $resourceClass, Operation $operation,
8588
return [];
8689
}
8790

88-
$fieldName = lcfirst('item_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
91+
$fieldName = lcfirst('item_query' === $operation->getName() ? ($operation->getShortName() ?? $operation->getName()) : $operation->getName().$operation->getShortName());
8992

9093
if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), Type::nullable(Type::object($resourceClass)), $resourceClass, false, $operation)) {
9194
$args = $this->resolveResourceArgs($configuration['args'] ?? [], $operation);
@@ -606,7 +609,8 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root
606609
continue;
607610
}
608611

609-
foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $description) {
612+
$entityClass = $this->getStateOptionsClass($resourceOperation, $resourceOperation->getClass());
613+
foreach ($this->filterLocator->get($filterId)->getDescription($entityClass) as $key => $description) {
610614
$filterType = \in_array($description['type'], TypeIdentifier::values(), true) ? Type::builtin($description['type']) : Type::object($description['type']);
611615
if (!($description['required'] ?? false)) {
612616
$filterType = Type::nullable($filterType);

src/Hydra/JsonSchema/SchemaFactory.php

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,7 @@ public function buildSchema(string $className, string $format = 'jsonld', string
170170

171171
$definitions[self::COLLECTION_BASE_SCHEMA_NAME] = [
172172
'type' => 'object',
173-
'required' => [
174-
$hydraPrefix.'member',
175-
],
176173
'properties' => [
177-
$hydraPrefix.'member' => [
178-
'type' => 'array',
179-
'items' => ['type' => 'object'],
180-
],
181174
$hydraPrefix.'totalItems' => [
182175
'type' => 'integer',
183176
'minimum' => 0,
@@ -249,6 +242,9 @@ public function buildSchema(string $className, string $format = 'jsonld', string
249242
['$ref' => $prefix.self::COLLECTION_BASE_SCHEMA_NAME],
250243
[
251244
'type' => 'object',
245+
'required' => [
246+
$hydraPrefix.'member',
247+
],
252248
'properties' => [
253249
$hydraPrefix.'member' => [
254250
'type' => 'array',

src/Hydra/Tests/JsonSchema/SchemaFactoryTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ public function testSchemaTypeBuildSchema(): void
132132
$this->assertNull($resultSchema->getRootDefinitionKey());
133133
$hydraCollectionSchema = $resultSchema['definitions']['HydraCollectionBaseSchema'];
134134
$properties = $hydraCollectionSchema['properties'];
135-
$this->assertTrue(isset($properties['hydra:member']));
136135
$this->assertArrayHasKey('hydra:totalItems', $properties);
137136
$this->assertArrayHasKey('hydra:view', $properties);
138137
$this->assertArrayHasKey('hydra:search', $properties);

src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ private function getJsonSchemaFromType(Type $type, ?bool $readableLink = null):
280280
TypeIdentifier::OBJECT => ['type' => 'object'],
281281
TypeIdentifier::RESOURCE => ['type' => 'string'],
282282
TypeIdentifier::CALLABLE => ['type' => 'string'],
283+
TypeIdentifier::MIXED => ['type' => 'string'],
283284
default => ['type' => 'null'],
284285
};
285286

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\JsonSchema\Tests\Fixtures;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
18+
#[ApiResource]
19+
class DummyWithMixed
20+
{
21+
public mixed $mixedProperty;
22+
public array $mixedArrayProperty;
23+
}

src/JsonSchema/Tests/Metadata/Property/Factory/SchemaPropertyMetadataFactoryTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory;
1717
use ApiPlatform\JsonSchema\Tests\Fixtures\DummyWithCustomOpenApiContext;
1818
use ApiPlatform\JsonSchema\Tests\Fixtures\DummyWithEnum;
19+
use ApiPlatform\JsonSchema\Tests\Fixtures\DummyWithMixed;
1920
use ApiPlatform\JsonSchema\Tests\Fixtures\DummyWithUnionTypeProperty;
2021
use ApiPlatform\JsonSchema\Tests\Fixtures\Enum\IntEnumAsIdentifier;
2122
use ApiPlatform\Metadata\ApiProperty;
@@ -167,4 +168,37 @@ public function testUnionTypeAnyOfIsArray(): void
167168

168169
$this->assertEquals($expectedSchema, $apiProperty->getSchema());
169170
}
171+
172+
public function testMixed(): void
173+
{
174+
if (!method_exists(PropertyInfoExtractor::class, 'getType')) { // @phpstan-ignore-line symfony/property-info 6.4 is still allowed and this may be true
175+
$this->markTestSkipped('This test only supports type-info component');
176+
}
177+
178+
$resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class);
179+
$apiProperty = new ApiProperty(nativeType: Type::mixed());
180+
$decorated = $this->createMock(PropertyMetadataFactoryInterface::class);
181+
$decorated->expects($this->once())->method('create')->with(DummyWithMixed::class, 'mixedProperty')->willReturn($apiProperty);
182+
183+
$schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated);
184+
$apiProperty = $schemaPropertyMetadataFactory->create(DummyWithMixed::class, 'mixedProperty');
185+
186+
$this->assertEquals([
187+
'type' => ['string', 'null'],
188+
], $apiProperty->getSchema());
189+
190+
$apiProperty = new ApiProperty(nativeType: Type::array(Type::mixed()));
191+
$decorated = $this->createMock(PropertyMetadataFactoryInterface::class);
192+
$decorated->expects($this->once())->method('create')->with(DummyWithMixed::class, 'mixedArrayProperty')->willReturn($apiProperty);
193+
194+
$schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated);
195+
$apiProperty = $schemaPropertyMetadataFactory->create(DummyWithMixed::class, 'mixedArrayProperty');
196+
197+
$this->assertEquals([
198+
'type' => 'array',
199+
'items' => [
200+
'type' => ['string', 'null'],
201+
],
202+
], $apiProperty->getSchema());
203+
}
170204
}

src/Laravel/ApiPlatformProvider.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@
8989
use ApiPlatform\Laravel\Eloquent\PropertyAccess\PropertyAccessor as EloquentPropertyAccessor;
9090
use ApiPlatform\Laravel\Eloquent\PropertyInfo\EloquentExtractor;
9191
use ApiPlatform\Laravel\Eloquent\Serializer\EloquentNameConverter;
92+
use ApiPlatform\Laravel\Eloquent\Serializer\Mapping\Loader\AttributeLoader as EloquentAttributeLoader;
93+
use ApiPlatform\Laravel\Eloquent\Serializer\Mapping\Loader\RelationMetadataLoader;
9294
use ApiPlatform\Laravel\Eloquent\Serializer\SerializerContextBuilder as EloquentSerializerContextBuilder;
9395
use ApiPlatform\Laravel\GraphQl\Controller\EntrypointController as GraphQlEntrypointController;
9496
use ApiPlatform\Laravel\GraphQl\Controller\GraphiQlController;
@@ -174,7 +176,6 @@
174176
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
175177
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
176178
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
177-
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
178179
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
179180
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
180181
use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter;
@@ -216,7 +217,6 @@ public function register(): void
216217
return new ModelMetadata();
217218
});
218219

219-
$this->app->bind(LoaderInterface::class, AttributeLoader::class);
220220
$this->app->bind(ClassMetadataFactoryInterface::class, ClassMetadataFactory::class);
221221
$this->app->singleton(ClassMetadataFactory::class, function (Application $app) {
222222
/** @var ConfigRepository */
@@ -232,8 +232,8 @@ public function register(): void
232232
$app->make(PropertyNameCollectionFactoryInterface::class),
233233
$nameConverter
234234
),
235-
new AttributeLoader(),
236-
// new RelationMetadataLoader($app->make(ModelMetadata::class)),
235+
new EloquentAttributeLoader(new AttributeLoader()),
236+
new RelationMetadataLoader($app->make(ModelMetadata::class)),
237237
])
238238
);
239239
});
@@ -330,8 +330,8 @@ public function register(): void
330330
);
331331
});
332332

333-
$this->app->bind(PropertyAccessorInterface::class, function () {
334-
return new EloquentPropertyAccessor();
333+
$this->app->bind(PropertyAccessorInterface::class, function (Application $app) {
334+
return new EloquentPropertyAccessor(null, $app->make(ModelMetadata::class));
335335
});
336336

337337
$this->app->bind(NameConverterInterface::class, function (Application $app) {
@@ -467,7 +467,7 @@ public function register(): void
467467
$config = $app['config'];
468468
$defaultContext = $config->get('api-platform.serializer', []);
469469

470-
return new ObjectNormalizer(defaultContext: $defaultContext);
470+
return new ObjectNormalizer($app->make(ClassMetadataFactoryInterface::class), defaultContext: $defaultContext);
471471
});
472472

473473
$this->app->singleton(DateTimeNormalizer::class, function (Application $app) {

0 commit comments

Comments
 (0)