Skip to content

Commit 88619ac

Browse files
committed
feat: defaults parameters
1 parent 4f6c4e1 commit 88619ac

File tree

6 files changed

+471
-1
lines changed

6 files changed

+471
-1
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\Metadata\Resource\Factory;
15+
16+
use ApiPlatform\Metadata\Parameter;
17+
use ApiPlatform\Metadata\Parameters;
18+
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
19+
20+
/**
21+
* Adds default parameters from the global configuration to all resources and operations.
22+
*
23+
* @author Maxence Castel <maxence.castel59@gmail.com>
24+
*/
25+
final class DefaultParametersResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
26+
{
27+
/**
28+
* @param array<string, array<string, mixed>> $defaultParameters Array where keys are parameter class names and values are their configuration
29+
*/
30+
public function __construct(
31+
private readonly array $defaultParameters = [],
32+
private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null,
33+
) {
34+
}
35+
36+
/**
37+
* {@inheritdoc}
38+
*/
39+
public function create(string $resourceClass): ResourceMetadataCollection
40+
{
41+
$resourceMetadataCollection = new ResourceMetadataCollection($resourceClass);
42+
43+
if ($this->decorated) {
44+
$resourceMetadataCollection = $this->decorated->create($resourceClass);
45+
}
46+
47+
if (empty($this->defaultParameters)) {
48+
return $resourceMetadataCollection;
49+
}
50+
51+
$defaultParams = $this->buildDefaultParameters();
52+
53+
foreach ($resourceMetadataCollection as $i => $resource) {
54+
$resourceParameters = $resource->getParameters() ?? new Parameters();
55+
$mergedResourceParameters = $this->mergeParameters($resourceParameters, $defaultParams);
56+
$resource = $resource->withParameters($mergedResourceParameters);
57+
58+
foreach ($operations = $resource->getOperations() ?? [] as $operationName => $operation) {
59+
$operationParameters = $operation->getParameters() ?? new Parameters();
60+
$mergedOperationParameters = $this->mergeParameters($operationParameters, $defaultParams);
61+
$operations->add((string) $operationName, $operation->withParameters($mergedOperationParameters));
62+
}
63+
64+
if ($operations) {
65+
$resource = $resource->withOperations($operations);
66+
}
67+
68+
foreach ($graphQlOperations = $resource->getGraphQlOperations() ?? [] as $operationName => $operation) {
69+
$operationParameters = $operation->getParameters() ?? new Parameters();
70+
$mergedOperationParameters = $this->mergeParameters($operationParameters, $defaultParams);
71+
$graphQlOperations[$operationName] = $operation->withParameters($mergedOperationParameters);
72+
}
73+
74+
if ($graphQlOperations) {
75+
$resource = $resource->withGraphQlOperations($graphQlOperations);
76+
}
77+
78+
$resourceMetadataCollection[$i] = $resource;
79+
}
80+
81+
return $resourceMetadataCollection;
82+
}
83+
84+
/**
85+
* Builds Parameter objects from the default configuration array.
86+
*
87+
* @return array<string, Parameter> Array of Parameter objects indexed by their key
88+
*/
89+
private function buildDefaultParameters(): array
90+
{
91+
$parameters = [];
92+
93+
foreach ($this->defaultParameters as $parameterClass => $config) {
94+
if (!is_subclass_of($parameterClass, Parameter::class)) {
95+
continue;
96+
}
97+
98+
$key = $config['key'] ?? null;
99+
if (!$key) {
100+
$key = (new \ReflectionClass($parameterClass))->getShortName();
101+
}
102+
103+
$identifier = $key;
104+
105+
$parameter = $this->createParameterFromConfig($parameterClass, $config);
106+
$parameters[$identifier] = $parameter;
107+
}
108+
109+
return $parameters;
110+
}
111+
112+
/**
113+
* Creates a Parameter instance from configuration.
114+
*
115+
* @param class-string<Parameter> $parameterClass The parameter class name
116+
* @param array<string, mixed> $config The configuration array
117+
*
118+
* @return Parameter The created parameter instance
119+
*/
120+
private function createParameterFromConfig(string $parameterClass, array $config): Parameter
121+
{
122+
return new $parameterClass(
123+
key: $config['key'] ?? null,
124+
schema: $config['schema'] ?? null,
125+
openApi: null,
126+
provider: null,
127+
filter: $config['filter'] ?? null,
128+
property: $config['property'] ?? null,
129+
description: $config['description'] ?? null,
130+
properties: null,
131+
required: $config['required'] ?? false,
132+
priority: $config['priority'] ?? null,
133+
hydra: $config['hydra'] ?? null,
134+
constraints: $config['constraints'] ?? null,
135+
security: $config['security'] ?? null,
136+
securityMessage: $config['security_message'] ?? null,
137+
extraProperties: $config['extra_properties'] ?? [],
138+
filterContext: null,
139+
nativeType: null,
140+
castToArray: null,
141+
castToNativeType: null,
142+
castFn: null,
143+
default: $config['default'] ?? null,
144+
filterClass: $config['filter_class'] ?? null,
145+
);
146+
}
147+
148+
/**
149+
* Merges default parameters with operation-specific parameters.
150+
*
151+
* @param Parameters $operationParameters The parameters already defined on the operation
152+
* @param array<string, Parameter> $defaultParams The default parameters to merge
153+
*
154+
* @return Parameters The merged parameters
155+
*/
156+
private function mergeParameters(Parameters $operationParameters, array $defaultParams): Parameters
157+
{
158+
$merged = new Parameters($defaultParams);
159+
160+
foreach ($operationParameters as $key => $param) {
161+
$merged->add($key, $param);
162+
}
163+
164+
return $merged;
165+
}
166+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\Metadata\Tests\Resource\Factory;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\GetCollection;
18+
use ApiPlatform\Metadata\HeaderParameter;
19+
use ApiPlatform\Metadata\QueryParameter;
20+
use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory;
21+
use ApiPlatform\Metadata\Resource\Factory\DefaultParametersResourceMetadataCollectionFactory;
22+
use PHPUnit\Framework\TestCase;
23+
24+
/**
25+
* Integration tests for DefaultParametersResourceMetadataCollectionFactory with real resources.
26+
*
27+
* @author Maxence Castel <maxence.castel59@gmail.com>
28+
*/
29+
final class DefaultParametersResourceMetadataCollectionFactoryTest extends TestCase
30+
{
31+
private const DEFAULT_PARAMETERS = [
32+
HeaderParameter::class => [
33+
'key' => 'X-API-Version',
34+
'required' => true,
35+
'description' => 'API Version',
36+
],
37+
];
38+
39+
public function testDefaultParametersAppliedToRealResource(): void
40+
{
41+
$attributesFactory = new AttributesResourceMetadataCollectionFactory();
42+
$defaultParametersFactory = new DefaultParametersResourceMetadataCollectionFactory(self::DEFAULT_PARAMETERS, $attributesFactory);
43+
44+
$resourceClass = TestProductResource::class;
45+
46+
$collection = $defaultParametersFactory->create($resourceClass);
47+
48+
$this->assertCount(1, $collection);
49+
$resource = $collection[0];
50+
$operations = $resource->getOperations();
51+
$this->assertNotNull($operations);
52+
53+
$collectionOperation = null;
54+
foreach ($operations as $operation) {
55+
if ($operation instanceof GetCollection) {
56+
$collectionOperation = $operation;
57+
break;
58+
}
59+
}
60+
61+
$this->assertNotNull($collectionOperation, 'GetCollection operation not found');
62+
63+
$parameters = $collectionOperation->getParameters();
64+
$this->assertNotNull($parameters);
65+
$this->assertTrue($parameters->has('X-API-Version', HeaderParameter::class), 'Default header parameter not found');
66+
67+
$headerParam = $parameters->get('X-API-Version', HeaderParameter::class);
68+
$this->assertSame('X-API-Version', $headerParam->getKey());
69+
$this->assertTrue($headerParam->getRequired());
70+
$this->assertSame('API Version', $headerParam->getDescription());
71+
}
72+
73+
public function testDefaultParametersWithOperationOverride(): void
74+
{
75+
$attributesFactory = new AttributesResourceMetadataCollectionFactory();
76+
$defaultParametersFactory = new DefaultParametersResourceMetadataCollectionFactory(self::DEFAULT_PARAMETERS, $attributesFactory);
77+
78+
$resourceClass = TestProductResourceWithParameters::class;
79+
80+
$collection = $defaultParametersFactory->create($resourceClass);
81+
82+
$this->assertCount(1, $collection);
83+
$resource = $collection[0];
84+
$operations = $resource->getOperations();
85+
$this->assertNotNull($operations);
86+
87+
$collectionOperation = null;
88+
foreach ($operations as $operation) {
89+
if ($operation instanceof GetCollection) {
90+
$collectionOperation = $operation;
91+
break;
92+
}
93+
}
94+
95+
$this->assertNotNull($collectionOperation);
96+
97+
$parameters = $collectionOperation->getParameters();
98+
$this->assertNotNull($parameters);
99+
100+
$this->assertTrue($parameters->has('X-API-Version', HeaderParameter::class));
101+
$this->assertTrue($parameters->has('filter', QueryParameter::class));
102+
}
103+
}
104+
105+
#[ApiResource(operations: [new GetCollection()])]
106+
class TestProductResource
107+
{
108+
public int $id = 1;
109+
public string $name = 'Test Product';
110+
}
111+
112+
#[ApiResource(
113+
operations: [
114+
new GetCollection(
115+
parameters: [
116+
'filter' => new QueryParameter(key: 'filter', description: 'Filter by name'),
117+
]
118+
),
119+
]
120+
)]
121+
class TestProductResourceWithParameters
122+
{
123+
public int $id = 1;
124+
public string $name = 'Test Product';
125+
}

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,9 @@ private function registerCommonConfiguration(ContainerBuilder $container, array
392392
$container->setAlias('api_platform.name_converter', $config['name_converter']);
393393
}
394394
$container->setParameter('api_platform.asset_package', $config['asset_package']);
395-
$container->setParameter('api_platform.defaults', $this->normalizeDefaults($config['defaults'] ?? []));
395+
$normalizedDefaults = $this->normalizeDefaults($config['defaults'] ?? []);
396+
$container->setParameter('api_platform.defaults', $normalizedDefaults);
397+
$container->setParameter('api_platform.defaults.parameters', $config['defaults']['parameters'] ?? []);
396398

397399
if ($container->getParameter('kernel.debug')) {
398400
$container->removeDefinition('api_platform.serializer.mapping.cache_class_metadata_factory');
@@ -421,6 +423,7 @@ private function normalizeDefaults(array $defaults): array
421423
{
422424
$normalizedDefaults = ['extra_properties' => $defaults['extra_properties'] ?? []];
423425
unset($defaults['extra_properties']);
426+
unset($defaults['parameters']);
424427

425428
$rc = new \ReflectionClass(ApiResource::class);
426429
$publicProperties = [];

src/Symfony/Bundle/DependencyInjection/Configuration.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface;
1717
use ApiPlatform\Metadata\ApiResource;
1818
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
19+
use ApiPlatform\Metadata\Parameter;
1920
use ApiPlatform\Metadata\Post;
2021
use ApiPlatform\Metadata\Put;
2122
use ApiPlatform\Symfony\Controller\MainController;
@@ -655,6 +656,18 @@ private function addDefaultsSection(ArrayNodeDefinition $rootNode): void
655656
$this->defineDefault($defaultsNode, new \ReflectionClass(ApiResource::class), $nameConverter);
656657
$this->defineDefault($defaultsNode, new \ReflectionClass(Put::class), $nameConverter);
657658
$this->defineDefault($defaultsNode, new \ReflectionClass(Post::class), $nameConverter);
659+
660+
$parametersNode = $defaultsNode
661+
->children()
662+
->arrayNode('parameters')
663+
->info('Global parameters applied to all resources and operations.')
664+
->useAttributeAsKey('parameter_class')
665+
->prototype('array')
666+
->ignoreExtraKeys(false);
667+
668+
$this->defineDefault($parametersNode, new \ReflectionClass(Parameter::class), $nameConverter);
669+
670+
$parametersNode->end()->end()->end();
658671
}
659672

660673
private function addMakerSection(ArrayNodeDefinition $rootNode): void

src/Symfony/Bundle/Resources/config/metadata/resource.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use ApiPlatform\Metadata\Resource\Factory\BackedEnumResourceMetadataCollectionFactory;
1919
use ApiPlatform\Metadata\Resource\Factory\CachedResourceMetadataCollectionFactory;
2020
use ApiPlatform\Metadata\Resource\Factory\ConcernsResourceMetadataCollectionFactory;
21+
use ApiPlatform\Metadata\Resource\Factory\DefaultParametersResourceMetadataCollectionFactory;
2122
use ApiPlatform\Metadata\Resource\Factory\ExtractorResourceMetadataCollectionFactory;
2223
use ApiPlatform\Metadata\Resource\Factory\FiltersResourceMetadataCollectionFactory;
2324
use ApiPlatform\Metadata\Resource\Factory\FormatsResourceMetadataCollectionFactory;
@@ -153,6 +154,13 @@
153154
service('logger')->ignoreOnInvalid(),
154155
]);
155156

157+
$services->set('api_platform.metadata.resource.metadata_collection_factory.default_parameters', DefaultParametersResourceMetadataCollectionFactory::class)
158+
->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 1001)
159+
->args([
160+
'%api_platform.defaults.parameters%',
161+
service('api_platform.metadata.resource.metadata_collection_factory.default_parameters.inner'),
162+
]);
163+
156164
$services->set('api_platform.metadata.resource.metadata_collection_factory.cached', CachedResourceMetadataCollectionFactory::class)
157165
->decorate('api_platform.metadata.resource.metadata_collection_factory', null, -10)
158166
->args([

0 commit comments

Comments
 (0)