Skip to content

Commit 57a733c

Browse files
committed
feat: defaults parameters
1 parent ed3629d commit 57a733c

6 files changed

Lines changed: 501 additions & 0 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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 Kévin Dunglas <dunglas@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+
required: $config['required'] ?? false,
125+
description: $config['description'] ?? null,
126+
property: $config['property'] ?? null,
127+
default: $config['default'] ?? null,
128+
schema: $config['schema'] ?? null,
129+
filter: $config['filter'] ?? null,
130+
priority: $config['priority'] ?? null,
131+
hydra: $config['hydra'] ?? null,
132+
constraints: $config['constraints'] ?? null,
133+
security: $config['security'] ?? null,
134+
securityMessage: $config['security_message'] ?? null,
135+
);
136+
}
137+
138+
/**
139+
* Merges default parameters with operation-specific parameters.
140+
*
141+
* @param Parameters $operationParameters The parameters already defined on the operation
142+
* @param array<string, Parameter> $defaultParams The default parameters to merge
143+
*
144+
* @return Parameters The merged parameters
145+
*/
146+
private function mergeParameters(Parameters $operationParameters, array $defaultParams): Parameters
147+
{
148+
$merged = new Parameters($defaultParams);
149+
150+
foreach ($operationParameters as $key => $param) {
151+
$merged->add($key, $param);
152+
}
153+
154+
return $merged;
155+
}
156+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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\Tests\Metadata\Resource\Factory;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\Get;
18+
use ApiPlatform\Metadata\GetCollection;
19+
use ApiPlatform\Metadata\HeaderParameter;
20+
use ApiPlatform\Metadata\Operations;
21+
use ApiPlatform\Metadata\QueryParameter;
22+
use ApiPlatform\Metadata\Resource\Factory\DefaultParametersResourceMetadataCollectionFactory;
23+
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
24+
use PHPUnit\Framework\TestCase;
25+
26+
/**
27+
* Tests for DefaultParametersResourceMetadataCollectionFactory.
28+
*
29+
* @author Kévin Dunglas <dunglas@gmail.com>
30+
*/
31+
final class DefaultParametersResourceMetadataCollectionFactoryTest extends TestCase
32+
{
33+
public function testAddDefaultHeaderParameter(): void
34+
{
35+
$defaultParameters = [
36+
HeaderParameter::class => [
37+
'key' => 'X-API-Version',
38+
'required' => true,
39+
'description' => 'API Version',
40+
],
41+
];
42+
43+
$factory = new DefaultParametersResourceMetadataCollectionFactory($defaultParameters);
44+
45+
$resource = (new ApiResource())
46+
->withClass('DummyResource')
47+
->withOperations(new Operations([
48+
new GetCollection(),
49+
new Get(uriTemplate: '/dummies/{id}'),
50+
]));
51+
52+
$collection = new ResourceMetadataCollection('DummyResource', [$resource]);
53+
54+
$result = $factory->create('DummyResource');
55+
56+
$this->assertInstanceOf(ResourceMetadataCollection::class, $result);
57+
}
58+
59+
public function testAddDefaultQueryParameter(): void
60+
{
61+
$defaultParameters = [
62+
QueryParameter::class => [
63+
'key' => 'filter',
64+
'required' => false,
65+
'description' => 'Filter results',
66+
],
67+
];
68+
69+
$factory = new DefaultParametersResourceMetadataCollectionFactory($defaultParameters);
70+
71+
$resource = (new ApiResource())
72+
->withClass('DummyResource')
73+
->withOperations(new Operations([
74+
new GetCollection(),
75+
]));
76+
77+
$collection = new ResourceMetadataCollection('DummyResource', [$resource]);
78+
79+
$result = $factory->create('DummyResource');
80+
81+
$this->assertInstanceOf(ResourceMetadataCollection::class, $result);
82+
}
83+
84+
public function testMultipleDefaultParameters(): void
85+
{
86+
$defaultParameters = [
87+
HeaderParameter::class => [
88+
'key' => 'X-API-Version',
89+
'required' => true,
90+
],
91+
QueryParameter::class => [
92+
'key' => 'sort',
93+
'required' => false,
94+
],
95+
];
96+
97+
$factory = new DefaultParametersResourceMetadataCollectionFactory($defaultParameters);
98+
99+
$resource = (new ApiResource())
100+
->withClass('DummyResource')
101+
->withOperations(new Operations([
102+
new GetCollection(),
103+
]));
104+
105+
$collection = new ResourceMetadataCollection('DummyResource', [$resource]);
106+
107+
$result = $factory->create('DummyResource');
108+
109+
$this->assertInstanceOf(ResourceMetadataCollection::class, $result);
110+
}
111+
112+
public function testEmptyDefaultParameters(): void
113+
{
114+
$factory = new DefaultParametersResourceMetadataCollectionFactory([]);
115+
116+
$resource = (new ApiResource())
117+
->withClass('DummyResource')
118+
->withOperations(new Operations([
119+
new GetCollection(),
120+
]));
121+
122+
$collection = new ResourceMetadataCollection('DummyResource', [$resource]);
123+
124+
$result = $factory->create('DummyResource');
125+
126+
$this->assertInstanceOf(ResourceMetadataCollection::class, $result);
127+
}
128+
129+
public function testDefaultParametersWithDecoratedFactory(): void
130+
{
131+
$defaultParameters = [
132+
HeaderParameter::class => [
133+
'key' => 'X-API-Version',
134+
'required' => true,
135+
],
136+
];
137+
138+
$mockDecorated = $this->createMock(\ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface::class);
139+
140+
$resource = (new ApiResource())
141+
->withClass('DummyResource')
142+
->withOperations(new Operations([
143+
new GetCollection(),
144+
]));
145+
146+
$collection = new ResourceMetadataCollection('DummyResource', [$resource]);
147+
148+
$mockDecorated->expects($this->once())->method('create')->with('DummyResource')->willReturn($collection);
149+
150+
$factory = new DefaultParametersResourceMetadataCollectionFactory($defaultParameters, $mockDecorated);
151+
152+
$result = $factory->create('DummyResource');
153+
154+
$this->assertInstanceOf(ResourceMetadataCollection::class, $result);
155+
}
156+
}

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,9 @@ private function getPaginationDefaults(array $defaults, array $collectionPaginat
420420
private function normalizeDefaults(array $defaults): array
421421
{
422422
$normalizedDefaults = ['extra_properties' => $defaults['extra_properties'] ?? []];
423+
$normalizedDefaults['parameters'] = $defaults['parameters'] ?? [];
423424
unset($defaults['extra_properties']);
425+
unset($defaults['parameters']);
424426

425427
$rc = new \ReflectionClass(ApiResource::class);
426428
$publicProperties = [];

src/Symfony/Bundle/DependencyInjection/Configuration.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,32 @@ private function addDefaultsSection(ArrayNodeDefinition $rootNode): void
655655
$this->defineDefault($defaultsNode, new \ReflectionClass(ApiResource::class), $nameConverter);
656656
$this->defineDefault($defaultsNode, new \ReflectionClass(Put::class), $nameConverter);
657657
$this->defineDefault($defaultsNode, new \ReflectionClass(Post::class), $nameConverter);
658+
659+
$defaultsNode
660+
->children()
661+
->arrayNode('parameters')
662+
->info('Global parameters to be applied to all resources and operations.')
663+
->useAttributeAsKey('parameter_class')
664+
->normalizeKeys(false)
665+
->prototype('array')
666+
->ignoreExtraKeys(false)
667+
->children()
668+
->scalarNode('key')->info('The parameter key/name.')->end()
669+
->booleanNode('required')->defaultFalse()->info('Whether the parameter is required.')->end()
670+
->scalarNode('description')->defaultNull()->info('The parameter description.')->end()
671+
->scalarNode('property')->defaultNull()->info('The property mapped to this parameter.')->end()
672+
->variableNode('default')->defaultNull()->info('The default value for the parameter.')->end()
673+
->variableNode('schema')->defaultNull()->info('The JSON schema for the parameter.')->end()
674+
->scalarNode('filter')->defaultNull()->info('The filter service ID for the parameter.')->end()
675+
->integerNode('priority')->defaultNull()->info('The parameter priority.')->end()
676+
->booleanNode('hydra')->defaultNull()->info('Whether to include this parameter in Hydra documentation.')->end()
677+
->variableNode('constraints')->defaultNull()->info('The validation constraints for the parameter.')->end()
678+
->scalarNode('security')->defaultNull()->info('The security expression for the parameter.')->end()
679+
->scalarNode('security_message')->defaultNull()->info('The security message for the parameter.')->end()
680+
->end()
681+
->end()
682+
->end()
683+
->end();
658684
}
659685

660686
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, 150)
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)