From e92a3973cdd2c711f69291c5bd5897c639fb87ff Mon Sep 17 00:00:00 2001 From: Maxcastel Date: Mon, 16 Feb 2026 16:39:26 +0100 Subject: [PATCH 1/4] feat: defaults parameters --- ...etersResourceMetadataCollectionFactory.php | 166 ++++++++++++++++++ ...sResourceMetadataCollectionFactoryTest.php | 125 +++++++++++++ .../ApiPlatformExtension.php | 5 +- .../DependencyInjection/Configuration.php | 13 ++ .../Resources/config/metadata/resource.php | 8 + .../ConfigurationDefaultParametersTest.php | 155 ++++++++++++++++ 6 files changed, 471 insertions(+), 1 deletion(-) create mode 100644 src/Metadata/Resource/Factory/DefaultParametersResourceMetadataCollectionFactory.php create mode 100644 src/Metadata/Tests/Resource/Factory/DefaultParametersResourceMetadataCollectionFactoryTest.php create mode 100644 tests/Symfony/Bundle/DependencyInjection/ConfigurationDefaultParametersTest.php diff --git a/src/Metadata/Resource/Factory/DefaultParametersResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/DefaultParametersResourceMetadataCollectionFactory.php new file mode 100644 index 0000000000..44dd77ac7a --- /dev/null +++ b/src/Metadata/Resource/Factory/DefaultParametersResourceMetadataCollectionFactory.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Metadata\Resource\Factory; + +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\Parameters; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; + +/** + * Adds default parameters from the global configuration to all resources and operations. + * + * @author Maxence Castel + */ +final class DefaultParametersResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface +{ + /** + * @param array> $defaultParameters Array where keys are parameter class names and values are their configuration + */ + public function __construct( + private readonly array $defaultParameters = [], + private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, + ) { + } + + /** + * {@inheritdoc} + */ + public function create(string $resourceClass): ResourceMetadataCollection + { + $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass); + + if ($this->decorated) { + $resourceMetadataCollection = $this->decorated->create($resourceClass); + } + + if (empty($this->defaultParameters)) { + return $resourceMetadataCollection; + } + + $defaultParams = $this->buildDefaultParameters(); + + foreach ($resourceMetadataCollection as $i => $resource) { + $resourceParameters = $resource->getParameters() ?? new Parameters(); + $mergedResourceParameters = $this->mergeParameters($resourceParameters, $defaultParams); + $resource = $resource->withParameters($mergedResourceParameters); + + foreach ($operations = $resource->getOperations() ?? [] as $operationName => $operation) { + $operationParameters = $operation->getParameters() ?? new Parameters(); + $mergedOperationParameters = $this->mergeParameters($operationParameters, $defaultParams); + $operations->add((string) $operationName, $operation->withParameters($mergedOperationParameters)); + } + + if ($operations) { + $resource = $resource->withOperations($operations); + } + + foreach ($graphQlOperations = $resource->getGraphQlOperations() ?? [] as $operationName => $operation) { + $operationParameters = $operation->getParameters() ?? new Parameters(); + $mergedOperationParameters = $this->mergeParameters($operationParameters, $defaultParams); + $graphQlOperations[$operationName] = $operation->withParameters($mergedOperationParameters); + } + + if ($graphQlOperations) { + $resource = $resource->withGraphQlOperations($graphQlOperations); + } + + $resourceMetadataCollection[$i] = $resource; + } + + return $resourceMetadataCollection; + } + + /** + * Builds Parameter objects from the default configuration array. + * + * @return array Array of Parameter objects indexed by their key + */ + private function buildDefaultParameters(): array + { + $parameters = []; + + foreach ($this->defaultParameters as $parameterClass => $config) { + if (!is_subclass_of($parameterClass, Parameter::class)) { + continue; + } + + $key = $config['key'] ?? null; + if (!$key) { + $key = (new \ReflectionClass($parameterClass))->getShortName(); + } + + $identifier = $key; + + $parameter = $this->createParameterFromConfig($parameterClass, $config); + $parameters[$identifier] = $parameter; + } + + return $parameters; + } + + /** + * Creates a Parameter instance from configuration. + * + * @param class-string $parameterClass The parameter class name + * @param array $config The configuration array + * + * @return Parameter The created parameter instance + */ + private function createParameterFromConfig(string $parameterClass, array $config): Parameter + { + return new $parameterClass( + key: $config['key'] ?? null, + schema: $config['schema'] ?? null, + openApi: null, + provider: null, + filter: $config['filter'] ?? null, + property: $config['property'] ?? null, + description: $config['description'] ?? null, + properties: null, + required: $config['required'] ?? false, + priority: $config['priority'] ?? null, + hydra: $config['hydra'] ?? null, + constraints: $config['constraints'] ?? null, + security: $config['security'] ?? null, + securityMessage: $config['security_message'] ?? null, + extraProperties: $config['extra_properties'] ?? [], + filterContext: null, + nativeType: null, + castToArray: null, + castToNativeType: null, + castFn: null, + default: $config['default'] ?? null, + filterClass: $config['filter_class'] ?? null, + ); + } + + /** + * Merges default parameters with operation-specific parameters. + * + * @param Parameters $operationParameters The parameters already defined on the operation + * @param array $defaultParams The default parameters to merge + * + * @return Parameters The merged parameters + */ + private function mergeParameters(Parameters $operationParameters, array $defaultParams): Parameters + { + $merged = new Parameters($defaultParams); + + foreach ($operationParameters as $key => $param) { + $merged->add($key, $param); + } + + return $merged; + } +} diff --git a/src/Metadata/Tests/Resource/Factory/DefaultParametersResourceMetadataCollectionFactoryTest.php b/src/Metadata/Tests/Resource/Factory/DefaultParametersResourceMetadataCollectionFactoryTest.php new file mode 100644 index 0000000000..831f096b48 --- /dev/null +++ b/src/Metadata/Tests/Resource/Factory/DefaultParametersResourceMetadataCollectionFactoryTest.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Metadata\Tests\Resource\Factory; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\HeaderParameter; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\DefaultParametersResourceMetadataCollectionFactory; +use PHPUnit\Framework\TestCase; + +/** + * Integration tests for DefaultParametersResourceMetadataCollectionFactory with real resources. + * + * @author Maxence Castel + */ +final class DefaultParametersResourceMetadataCollectionFactoryTest extends TestCase +{ + private const DEFAULT_PARAMETERS = [ + HeaderParameter::class => [ + 'key' => 'X-API-Version', + 'required' => true, + 'description' => 'API Version', + ], + ]; + + public function testDefaultParametersAppliedToRealResource(): void + { + $attributesFactory = new AttributesResourceMetadataCollectionFactory(); + $defaultParametersFactory = new DefaultParametersResourceMetadataCollectionFactory(self::DEFAULT_PARAMETERS, $attributesFactory); + + $resourceClass = TestProductResource::class; + + $collection = $defaultParametersFactory->create($resourceClass); + + $this->assertCount(1, $collection); + $resource = $collection[0]; + $operations = $resource->getOperations(); + $this->assertNotNull($operations); + + $collectionOperation = null; + foreach ($operations as $operation) { + if ($operation instanceof GetCollection) { + $collectionOperation = $operation; + break; + } + } + + $this->assertNotNull($collectionOperation, 'GetCollection operation not found'); + + $parameters = $collectionOperation->getParameters(); + $this->assertNotNull($parameters); + $this->assertTrue($parameters->has('X-API-Version', HeaderParameter::class), 'Default header parameter not found'); + + $headerParam = $parameters->get('X-API-Version', HeaderParameter::class); + $this->assertSame('X-API-Version', $headerParam->getKey()); + $this->assertTrue($headerParam->getRequired()); + $this->assertSame('API Version', $headerParam->getDescription()); + } + + public function testDefaultParametersWithOperationOverride(): void + { + $attributesFactory = new AttributesResourceMetadataCollectionFactory(); + $defaultParametersFactory = new DefaultParametersResourceMetadataCollectionFactory(self::DEFAULT_PARAMETERS, $attributesFactory); + + $resourceClass = TestProductResourceWithParameters::class; + + $collection = $defaultParametersFactory->create($resourceClass); + + $this->assertCount(1, $collection); + $resource = $collection[0]; + $operations = $resource->getOperations(); + $this->assertNotNull($operations); + + $collectionOperation = null; + foreach ($operations as $operation) { + if ($operation instanceof GetCollection) { + $collectionOperation = $operation; + break; + } + } + + $this->assertNotNull($collectionOperation); + + $parameters = $collectionOperation->getParameters(); + $this->assertNotNull($parameters); + + $this->assertTrue($parameters->has('X-API-Version', HeaderParameter::class)); + $this->assertTrue($parameters->has('filter', QueryParameter::class)); + } +} + +#[ApiResource(operations: [new GetCollection()])] +class TestProductResource +{ + public int $id = 1; + public string $name = 'Test Product'; +} + +#[ApiResource( + operations: [ + new GetCollection( + parameters: [ + 'filter' => new QueryParameter(key: 'filter', description: 'Filter by name'), + ] + ), + ] +)] +class TestProductResourceWithParameters +{ + public int $id = 1; + public string $name = 'Test Product'; +} diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 1f2f6689b9..c02a0786c6 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -392,7 +392,9 @@ private function registerCommonConfiguration(ContainerBuilder $container, array $container->setAlias('api_platform.name_converter', $config['name_converter']); } $container->setParameter('api_platform.asset_package', $config['asset_package']); - $container->setParameter('api_platform.defaults', $this->normalizeDefaults($config['defaults'] ?? [])); + $normalizedDefaults = $this->normalizeDefaults($config['defaults'] ?? []); + $container->setParameter('api_platform.defaults', $normalizedDefaults); + $container->setParameter('api_platform.defaults.parameters', $config['defaults']['parameters'] ?? []); if ($container->getParameter('kernel.debug')) { $container->removeDefinition('api_platform.serializer.mapping.cache_class_metadata_factory'); @@ -421,6 +423,7 @@ private function normalizeDefaults(array $defaults): array { $normalizedDefaults = ['extra_properties' => $defaults['extra_properties'] ?? []]; unset($defaults['extra_properties']); + unset($defaults['parameters']); $rc = new \ReflectionClass(ApiResource::class); $publicProperties = []; diff --git a/src/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DependencyInjection/Configuration.php index a3ecfc1407..27cc86a9d4 100644 --- a/src/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DependencyInjection/Configuration.php @@ -16,6 +16,7 @@ use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Parameter; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Put; use ApiPlatform\Symfony\Controller\MainController; @@ -655,6 +656,18 @@ private function addDefaultsSection(ArrayNodeDefinition $rootNode): void $this->defineDefault($defaultsNode, new \ReflectionClass(ApiResource::class), $nameConverter); $this->defineDefault($defaultsNode, new \ReflectionClass(Put::class), $nameConverter); $this->defineDefault($defaultsNode, new \ReflectionClass(Post::class), $nameConverter); + + $parametersNode = $defaultsNode + ->children() + ->arrayNode('parameters') + ->info('Global parameters applied to all resources and operations.') + ->useAttributeAsKey('parameter_class') + ->prototype('array') + ->ignoreExtraKeys(false); + + $this->defineDefault($parametersNode, new \ReflectionClass(Parameter::class), $nameConverter); + + $parametersNode->end()->end()->end(); } private function addMakerSection(ArrayNodeDefinition $rootNode): void diff --git a/src/Symfony/Bundle/Resources/config/metadata/resource.php b/src/Symfony/Bundle/Resources/config/metadata/resource.php index 0e0b3c088d..5687db81ca 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/resource.php +++ b/src/Symfony/Bundle/Resources/config/metadata/resource.php @@ -18,6 +18,7 @@ use ApiPlatform\Metadata\Resource\Factory\BackedEnumResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\CachedResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\ConcernsResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\DefaultParametersResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\ExtractorResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\FiltersResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\FormatsResourceMetadataCollectionFactory; @@ -153,6 +154,13 @@ service('logger')->ignoreOnInvalid(), ]); + $services->set('api_platform.metadata.resource.metadata_collection_factory.default_parameters', DefaultParametersResourceMetadataCollectionFactory::class) + ->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 1001) + ->args([ + '%api_platform.defaults.parameters%', + service('api_platform.metadata.resource.metadata_collection_factory.default_parameters.inner'), + ]); + $services->set('api_platform.metadata.resource.metadata_collection_factory.cached', CachedResourceMetadataCollectionFactory::class) ->decorate('api_platform.metadata.resource.metadata_collection_factory', null, -10) ->args([ diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationDefaultParametersTest.php b/tests/Symfony/Bundle/DependencyInjection/ConfigurationDefaultParametersTest.php new file mode 100644 index 0000000000..d28f07f2aa --- /dev/null +++ b/tests/Symfony/Bundle/DependencyInjection/ConfigurationDefaultParametersTest.php @@ -0,0 +1,155 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Symfony\Bundle\DependencyInjection; + +use ApiPlatform\Symfony\Bundle\DependencyInjection\Configuration; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Definition\Processor; + +/** + * Tests the defaults.parameters configuration option. + * + * @author Maxence Castel + */ +final class ConfigurationDefaultParametersTest extends TestCase +{ + private Configuration $configuration; + + private Processor $processor; + + protected function setUp(): void + { + $this->configuration = new Configuration(); + $this->processor = new Processor(); + } + + public function testDefaultHeaderParameterConfiguration(): void + { + $config = $this->processor->processConfiguration($this->configuration, [ + 'api_platform' => [ + 'defaults' => [ + 'parameters' => [ + 'ApiPlatform\Metadata\HeaderParameter' => [ + 'key' => 'X-API-Version', + 'required' => true, + 'description' => 'API Version', + ], + ], + ], + ], + ]); + + $this->assertIsArray($config['defaults']['parameters']); + $this->assertArrayHasKey('ApiPlatform\Metadata\HeaderParameter', $config['defaults']['parameters']); + $this->assertSame('X-API-Version', $config['defaults']['parameters']['ApiPlatform\Metadata\HeaderParameter']['key']); + $this->assertTrue($config['defaults']['parameters']['ApiPlatform\Metadata\HeaderParameter']['required']); + $this->assertSame('API Version', $config['defaults']['parameters']['ApiPlatform\Metadata\HeaderParameter']['description']); + } + + public function testMultipleDefaultParametersConfiguration(): void + { + $config = $this->processor->processConfiguration($this->configuration, [ + 'api_platform' => [ + 'defaults' => [ + 'parameters' => [ + 'ApiPlatform\Metadata\HeaderParameter' => [ + 'key' => 'X-API-Version', + 'required' => true, + ], + 'ApiPlatform\Metadata\QueryParameter' => [ + 'key' => 'sort', + 'required' => false, + ], + ], + ], + ], + ]); + + $this->assertCount(2, $config['defaults']['parameters']); + $this->assertArrayHasKey('ApiPlatform\Metadata\HeaderParameter', $config['defaults']['parameters']); + $this->assertArrayHasKey('ApiPlatform\Metadata\QueryParameter', $config['defaults']['parameters']); + } + + public function testDefaultParametersWithAllOptions(): void + { + $config = $this->processor->processConfiguration($this->configuration, [ + 'api_platform' => [ + 'defaults' => [ + 'parameters' => [ + 'ApiPlatform\Metadata\HeaderParameter' => [ + 'key' => 'X-API-Version', + 'required' => true, + 'description' => 'API Version', + 'property' => 'version', + 'default' => '1.0', + 'filter' => 'api_platform.filter.version', + 'priority' => 10, + 'hydra' => true, + 'constraints' => ['NotNull'], + 'security' => 'is_granted("ROLE_ADMIN")', + 'security_message' => 'Access denied', + ], + ], + ], + ], + ]); + + $params = $config['defaults']['parameters']['ApiPlatform\Metadata\HeaderParameter']; + $this->assertSame('X-API-Version', $params['key']); + $this->assertTrue($params['required']); + $this->assertSame('API Version', $params['description']); + $this->assertSame('version', $params['property']); + $this->assertSame('1.0', $params['default']); + $this->assertSame('api_platform.filter.version', $params['filter']); + $this->assertSame(10, $params['priority']); + $this->assertTrue($params['hydra']); + $this->assertSame(['NotNull'], $params['constraints']); + $this->assertSame('is_granted("ROLE_ADMIN")', $params['security']); + $this->assertSame('Access denied', $params['security_message']); + } + + public function testEmptyDefaultParameters(): void + { + $config = $this->processor->processConfiguration($this->configuration, [ + 'api_platform' => [ + 'defaults' => [ + 'parameters' => [], + ], + ], + ]); + + $this->assertIsArray($config['defaults']['parameters']); + $this->assertEmpty($config['defaults']['parameters']); + } + + public function testDefaultParametersDoesNotAffectOtherDefaults(): void + { + $config = $this->processor->processConfiguration($this->configuration, [ + 'api_platform' => [ + 'defaults' => [ + 'parameters' => [ + 'ApiPlatform\Metadata\HeaderParameter' => [ + 'key' => 'X-API-Version', + 'required' => true, + ], + ], + 'pagination_items_per_page' => 50, + ], + ], + ]); + + $this->assertSame(50, $config['defaults']['pagination_items_per_page']); + $this->assertArrayHasKey('ApiPlatform\Metadata\HeaderParameter', $config['defaults']['parameters']); + } +} From c532bc645c8412d4b5fe22d38c3a1c15aa94c93d Mon Sep 17 00:00:00 2001 From: Maxcastel Date: Wed, 18 Feb 2026 16:14:02 +0100 Subject: [PATCH 2/4] refactor: merge DefaultParametersResourceMetadataCollectionFactory into ParameterValidationResourceMetadataCollectionFactory --- ...etersResourceMetadataCollectionFactory.php | 166 ------------------ .../Resources/config/metadata/resource.php | 8 - .../Resources/config/validator/validator.php | 1 + ...ationResourceMetadataCollectionFactory.php | 123 +++++++++++++ .../ParameterValidationConfigurationTest.php} | 6 +- ...CollectionFactoryDefaultParametersTest.php | 24 ++- 6 files changed, 143 insertions(+), 185 deletions(-) delete mode 100644 src/Metadata/Resource/Factory/DefaultParametersResourceMetadataCollectionFactory.php rename tests/{Symfony/Bundle/DependencyInjection/ConfigurationDefaultParametersTest.php => Validator/Metadata/Resource/Factory/ParameterValidationConfigurationTest.php} (95%) rename src/Metadata/Tests/Resource/Factory/DefaultParametersResourceMetadataCollectionFactoryTest.php => tests/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactoryDefaultParametersTest.php (79%) diff --git a/src/Metadata/Resource/Factory/DefaultParametersResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/DefaultParametersResourceMetadataCollectionFactory.php deleted file mode 100644 index 44dd77ac7a..0000000000 --- a/src/Metadata/Resource/Factory/DefaultParametersResourceMetadataCollectionFactory.php +++ /dev/null @@ -1,166 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Metadata\Resource\Factory; - -use ApiPlatform\Metadata\Parameter; -use ApiPlatform\Metadata\Parameters; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; - -/** - * Adds default parameters from the global configuration to all resources and operations. - * - * @author Maxence Castel - */ -final class DefaultParametersResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface -{ - /** - * @param array> $defaultParameters Array where keys are parameter class names and values are their configuration - */ - public function __construct( - private readonly array $defaultParameters = [], - private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, - ) { - } - - /** - * {@inheritdoc} - */ - public function create(string $resourceClass): ResourceMetadataCollection - { - $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass); - - if ($this->decorated) { - $resourceMetadataCollection = $this->decorated->create($resourceClass); - } - - if (empty($this->defaultParameters)) { - return $resourceMetadataCollection; - } - - $defaultParams = $this->buildDefaultParameters(); - - foreach ($resourceMetadataCollection as $i => $resource) { - $resourceParameters = $resource->getParameters() ?? new Parameters(); - $mergedResourceParameters = $this->mergeParameters($resourceParameters, $defaultParams); - $resource = $resource->withParameters($mergedResourceParameters); - - foreach ($operations = $resource->getOperations() ?? [] as $operationName => $operation) { - $operationParameters = $operation->getParameters() ?? new Parameters(); - $mergedOperationParameters = $this->mergeParameters($operationParameters, $defaultParams); - $operations->add((string) $operationName, $operation->withParameters($mergedOperationParameters)); - } - - if ($operations) { - $resource = $resource->withOperations($operations); - } - - foreach ($graphQlOperations = $resource->getGraphQlOperations() ?? [] as $operationName => $operation) { - $operationParameters = $operation->getParameters() ?? new Parameters(); - $mergedOperationParameters = $this->mergeParameters($operationParameters, $defaultParams); - $graphQlOperations[$operationName] = $operation->withParameters($mergedOperationParameters); - } - - if ($graphQlOperations) { - $resource = $resource->withGraphQlOperations($graphQlOperations); - } - - $resourceMetadataCollection[$i] = $resource; - } - - return $resourceMetadataCollection; - } - - /** - * Builds Parameter objects from the default configuration array. - * - * @return array Array of Parameter objects indexed by their key - */ - private function buildDefaultParameters(): array - { - $parameters = []; - - foreach ($this->defaultParameters as $parameterClass => $config) { - if (!is_subclass_of($parameterClass, Parameter::class)) { - continue; - } - - $key = $config['key'] ?? null; - if (!$key) { - $key = (new \ReflectionClass($parameterClass))->getShortName(); - } - - $identifier = $key; - - $parameter = $this->createParameterFromConfig($parameterClass, $config); - $parameters[$identifier] = $parameter; - } - - return $parameters; - } - - /** - * Creates a Parameter instance from configuration. - * - * @param class-string $parameterClass The parameter class name - * @param array $config The configuration array - * - * @return Parameter The created parameter instance - */ - private function createParameterFromConfig(string $parameterClass, array $config): Parameter - { - return new $parameterClass( - key: $config['key'] ?? null, - schema: $config['schema'] ?? null, - openApi: null, - provider: null, - filter: $config['filter'] ?? null, - property: $config['property'] ?? null, - description: $config['description'] ?? null, - properties: null, - required: $config['required'] ?? false, - priority: $config['priority'] ?? null, - hydra: $config['hydra'] ?? null, - constraints: $config['constraints'] ?? null, - security: $config['security'] ?? null, - securityMessage: $config['security_message'] ?? null, - extraProperties: $config['extra_properties'] ?? [], - filterContext: null, - nativeType: null, - castToArray: null, - castToNativeType: null, - castFn: null, - default: $config['default'] ?? null, - filterClass: $config['filter_class'] ?? null, - ); - } - - /** - * Merges default parameters with operation-specific parameters. - * - * @param Parameters $operationParameters The parameters already defined on the operation - * @param array $defaultParams The default parameters to merge - * - * @return Parameters The merged parameters - */ - private function mergeParameters(Parameters $operationParameters, array $defaultParams): Parameters - { - $merged = new Parameters($defaultParams); - - foreach ($operationParameters as $key => $param) { - $merged->add($key, $param); - } - - return $merged; - } -} diff --git a/src/Symfony/Bundle/Resources/config/metadata/resource.php b/src/Symfony/Bundle/Resources/config/metadata/resource.php index 5687db81ca..0e0b3c088d 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/resource.php +++ b/src/Symfony/Bundle/Resources/config/metadata/resource.php @@ -18,7 +18,6 @@ use ApiPlatform\Metadata\Resource\Factory\BackedEnumResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\CachedResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\ConcernsResourceMetadataCollectionFactory; -use ApiPlatform\Metadata\Resource\Factory\DefaultParametersResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\ExtractorResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\FiltersResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\FormatsResourceMetadataCollectionFactory; @@ -154,13 +153,6 @@ service('logger')->ignoreOnInvalid(), ]); - $services->set('api_platform.metadata.resource.metadata_collection_factory.default_parameters', DefaultParametersResourceMetadataCollectionFactory::class) - ->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 1001) - ->args([ - '%api_platform.defaults.parameters%', - service('api_platform.metadata.resource.metadata_collection_factory.default_parameters.inner'), - ]); - $services->set('api_platform.metadata.resource.metadata_collection_factory.cached', CachedResourceMetadataCollectionFactory::class) ->decorate('api_platform.metadata.resource.metadata_collection_factory', null, -10) ->args([ diff --git a/src/Symfony/Bundle/Resources/config/validator/validator.php b/src/Symfony/Bundle/Resources/config/validator/validator.php index 786221ea97..9372bdb234 100644 --- a/src/Symfony/Bundle/Resources/config/validator/validator.php +++ b/src/Symfony/Bundle/Resources/config/validator/validator.php @@ -37,5 +37,6 @@ ->args([ service('api_platform.validator.metadata.resource.metadata_collection_factory.parameter.inner'), service('api_platform.filter_locator'), + '%api_platform.defaults.parameters%', ]); }; diff --git a/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php b/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php index 27bc2b2dc1..bd3ef61fc4 100644 --- a/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php +++ b/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Validator\Metadata\Resource\Factory; +use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Parameter; use ApiPlatform\Metadata\Parameters; @@ -30,6 +31,7 @@ final class ParameterValidationResourceMetadataCollectionFactory implements Reso public function __construct( private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, private readonly ?ContainerInterface $filterLocator = null, + private readonly array $defaultParameters = [], ) { } @@ -37,7 +39,11 @@ public function create(string $resourceClass): ResourceMetadataCollection { $resourceMetadataCollection = $this->decorated?->create($resourceClass) ?? new ResourceMetadataCollection($resourceClass); + $defaultParams = $this->buildDefaultParameters(); + foreach ($resourceMetadataCollection as $i => $resource) { + $resource = $this->applyDefaults($resource, $defaultParams); + $operations = $resource->getOperations(); foreach ($operations as $operationName => $operation) { @@ -135,4 +141,121 @@ private function addFilterValidation(HttpOperation $operation): Parameters return $parameters; } + + /** + * Builds Parameter objects from the default configuration array. + * + * @return array Array of Parameter objects indexed by their key + */ + private function buildDefaultParameters(): array + { + $parameters = []; + + foreach ($this->defaultParameters as $parameterClass => $config) { + if (!is_subclass_of($parameterClass, Parameter::class)) { + continue; + } + + $key = $config['key'] ?? null; + if (!$key) { + $key = (new \ReflectionClass($parameterClass))->getShortName(); + } + + $identifier = $key; + + $parameter = $this->createParameterFromConfig($parameterClass, $config); + $parameters[$identifier] = $parameter; + } + + return $parameters; + } + + /** + * Creates a Parameter instance from configuration. + * + * @param class-string $parameterClass The parameter class name + * @param array $config The configuration array + * + * @return Parameter The created parameter instance + */ + private function createParameterFromConfig(string $parameterClass, array $config): Parameter + { + return new $parameterClass( + key: $config['key'] ?? null, + schema: $config['schema'] ?? null, + openApi: null, + provider: null, + filter: $config['filter'] ?? null, + property: $config['property'] ?? null, + description: $config['description'] ?? null, + properties: null, + required: $config['required'] ?? false, + priority: $config['priority'] ?? null, + hydra: $config['hydra'] ?? null, + constraints: $config['constraints'] ?? null, + security: $config['security'] ?? null, + securityMessage: $config['security_message'] ?? null, + extraProperties: $config['extra_properties'] ?? [], + filterContext: null, + nativeType: null, + castToArray: null, + castToNativeType: null, + castFn: null, + default: $config['default'] ?? null, + filterClass: $config['filter_class'] ?? null, + ); + } + + /** + * Applies default parameters to the resource. + * + * @param array $defaultParams The default parameters to apply + */ + private function applyDefaults(ApiResource $resource, array $defaultParams): ApiResource + { + $resourceParameters = $resource->getParameters() ?? new Parameters(); + $mergedResourceParameters = $this->mergeParameters($resourceParameters, $defaultParams); + $resource = $resource->withParameters($mergedResourceParameters); + + foreach ($operations = $resource->getOperations() ?? [] as $operationName => $operation) { + $operationParameters = $operation->getParameters() ?? new Parameters(); + $mergedOperationParameters = $this->mergeParameters($operationParameters, $defaultParams); + $operations->add((string) $operationName, $operation->withParameters($mergedOperationParameters)); + } + + if ($operations) { + $resource = $resource->withOperations($operations); + } + + foreach ($graphQlOperations = $resource->getGraphQlOperations() ?? [] as $operationName => $operation) { + $operationParameters = $operation->getParameters() ?? new Parameters(); + $mergedOperationParameters = $this->mergeParameters($operationParameters, $defaultParams); + $graphQlOperations[$operationName] = $operation->withParameters($mergedOperationParameters); + } + + if ($graphQlOperations) { + $resource = $resource->withGraphQlOperations($graphQlOperations); + } + + return $resource; + } + + /** + * Merges default parameters with operation-specific parameters. + * + * @param Parameters $operationParameters The parameters already defined on the operation + * @param array $defaultParams The default parameters to merge + * + * @return Parameters The merged parameters + */ + private function mergeParameters(Parameters $operationParameters, array $defaultParams): Parameters + { + $merged = new Parameters($defaultParams); + + foreach ($operationParameters as $key => $param) { + $merged->add($key, $param); + } + + return $merged; + } } diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationDefaultParametersTest.php b/tests/Validator/Metadata/Resource/Factory/ParameterValidationConfigurationTest.php similarity index 95% rename from tests/Symfony/Bundle/DependencyInjection/ConfigurationDefaultParametersTest.php rename to tests/Validator/Metadata/Resource/Factory/ParameterValidationConfigurationTest.php index d28f07f2aa..bb434e0d8d 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ConfigurationDefaultParametersTest.php +++ b/tests/Validator/Metadata/Resource/Factory/ParameterValidationConfigurationTest.php @@ -11,18 +11,18 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Symfony\Bundle\DependencyInjection; +namespace ApiPlatform\Tests\Validator\Metadata\Resource\Factory; use ApiPlatform\Symfony\Bundle\DependencyInjection\Configuration; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Definition\Processor; /** - * Tests the defaults.parameters configuration option. + * Tests the defaults.parameters configuration option used by ParameterValidationResourceMetadataCollectionFactory. * * @author Maxence Castel */ -final class ConfigurationDefaultParametersTest extends TestCase +final class ParameterValidationConfigurationTest extends TestCase { private Configuration $configuration; diff --git a/src/Metadata/Tests/Resource/Factory/DefaultParametersResourceMetadataCollectionFactoryTest.php b/tests/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactoryDefaultParametersTest.php similarity index 79% rename from src/Metadata/Tests/Resource/Factory/DefaultParametersResourceMetadataCollectionFactoryTest.php rename to tests/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactoryDefaultParametersTest.php index 831f096b48..9cd0a2bec0 100644 --- a/src/Metadata/Tests/Resource/Factory/DefaultParametersResourceMetadataCollectionFactoryTest.php +++ b/tests/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactoryDefaultParametersTest.php @@ -11,22 +11,22 @@ declare(strict_types=1); -namespace ApiPlatform\Metadata\Tests\Resource\Factory; +namespace ApiPlatform\Tests\Validator\Metadata\Resource\Factory; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\HeaderParameter; use ApiPlatform\Metadata\QueryParameter; use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory; -use ApiPlatform\Metadata\Resource\Factory\DefaultParametersResourceMetadataCollectionFactory; +use ApiPlatform\Validator\Metadata\Resource\Factory\ParameterValidationResourceMetadataCollectionFactory; use PHPUnit\Framework\TestCase; /** - * Integration tests for DefaultParametersResourceMetadataCollectionFactory with real resources. + * Integration tests for ParameterValidationResourceMetadataCollectionFactory with default parameters. * * @author Maxence Castel */ -final class DefaultParametersResourceMetadataCollectionFactoryTest extends TestCase +final class ParameterValidationResourceMetadataCollectionFactoryDefaultParametersTest extends TestCase { private const DEFAULT_PARAMETERS = [ HeaderParameter::class => [ @@ -39,11 +39,15 @@ final class DefaultParametersResourceMetadataCollectionFactoryTest extends TestC public function testDefaultParametersAppliedToRealResource(): void { $attributesFactory = new AttributesResourceMetadataCollectionFactory(); - $defaultParametersFactory = new DefaultParametersResourceMetadataCollectionFactory(self::DEFAULT_PARAMETERS, $attributesFactory); + $parameterValidationFactory = new ParameterValidationResourceMetadataCollectionFactory( + $attributesFactory, + null, + self::DEFAULT_PARAMETERS + ); $resourceClass = TestProductResource::class; - $collection = $defaultParametersFactory->create($resourceClass); + $collection = $parameterValidationFactory->create($resourceClass); $this->assertCount(1, $collection); $resource = $collection[0]; @@ -73,11 +77,15 @@ public function testDefaultParametersAppliedToRealResource(): void public function testDefaultParametersWithOperationOverride(): void { $attributesFactory = new AttributesResourceMetadataCollectionFactory(); - $defaultParametersFactory = new DefaultParametersResourceMetadataCollectionFactory(self::DEFAULT_PARAMETERS, $attributesFactory); + $parameterValidationFactory = new ParameterValidationResourceMetadataCollectionFactory( + $attributesFactory, + null, + self::DEFAULT_PARAMETERS + ); $resourceClass = TestProductResourceWithParameters::class; - $collection = $defaultParametersFactory->create($resourceClass); + $collection = $parameterValidationFactory->create($resourceClass); $this->assertCount(1, $collection); $resource = $collection[0]; From bb179ca38aea45730083a768f2509d30c3651957 Mon Sep 17 00:00:00 2001 From: Maxcastel Date: Wed, 18 Feb 2026 16:47:59 +0100 Subject: [PATCH 3/4] refactor: create array and name convert all keys --- ...ationResourceMetadataCollectionFactory.php | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php b/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php index bd3ef61fc4..f49e245511 100644 --- a/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php +++ b/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php @@ -23,6 +23,7 @@ use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; use ApiPlatform\Validator\Util\ParameterValidationConstraints; use Psr\Container\ContainerInterface; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; final class ParameterValidationResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { @@ -180,30 +181,18 @@ private function buildDefaultParameters(): array */ private function createParameterFromConfig(string $parameterClass, array $config): Parameter { - return new $parameterClass( - key: $config['key'] ?? null, - schema: $config['schema'] ?? null, - openApi: null, - provider: null, - filter: $config['filter'] ?? null, - property: $config['property'] ?? null, - description: $config['description'] ?? null, - properties: null, - required: $config['required'] ?? false, - priority: $config['priority'] ?? null, - hydra: $config['hydra'] ?? null, - constraints: $config['constraints'] ?? null, - security: $config['security'] ?? null, - securityMessage: $config['security_message'] ?? null, - extraProperties: $config['extra_properties'] ?? [], - filterContext: null, - nativeType: null, - castToArray: null, - castToNativeType: null, - castFn: null, - default: $config['default'] ?? null, - filterClass: $config['filter_class'] ?? null, - ); + $nameConverter = new CamelCaseToSnakeCaseNameConverter(); + $reflectionClass = new \ReflectionClass($parameterClass); + $constructor = $reflectionClass->getConstructor(); + + $args = []; + foreach ($constructor->getParameters() as $param) { + $paramName = $param->getName(); + $configKey = $nameConverter->normalize($paramName); + $args[$paramName] = $config[$configKey] ?? $param->getDefaultValue(); + } + + return new $parameterClass(...$args); } /** From a765758a67d9b0a690c7efbc5e10be160c808729 Mon Sep 17 00:00:00 2001 From: Maxcastel Date: Thu, 19 Feb 2026 11:11:35 +0100 Subject: [PATCH 4/4] test: add OpenApi and JsonSchema tests for defaults parameters --- .../CollectionFiltersNormalizer.php | 18 ++- ...ationResourceMetadataCollectionFactory.php | 7 +- .../Functional/DefaultParametersAppKernel.php | 51 ++++++ tests/Functional/DefaultParametersTest.php | 153 ++++++++++++++++++ .../JsonSchemaWithDefaultParametersTest.php | 80 +++++++++ 5 files changed, 305 insertions(+), 4 deletions(-) create mode 100644 tests/Functional/DefaultParametersAppKernel.php create mode 100644 tests/Functional/DefaultParametersTest.php create mode 100644 tests/Functional/JsonSchema/JsonSchemaWithDefaultParametersTest.php diff --git a/src/Hydra/Serializer/CollectionFiltersNormalizer.php b/src/Hydra/Serializer/CollectionFiltersNormalizer.php index 9625e99226..34121f97f2 100644 --- a/src/Hydra/Serializer/CollectionFiltersNormalizer.php +++ b/src/Hydra/Serializer/CollectionFiltersNormalizer.php @@ -17,6 +17,7 @@ use ApiPlatform\Hydra\State\Util\SearchHelperTrait; use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait; use ApiPlatform\Metadata\FilterInterface; +use ApiPlatform\Metadata\QueryParameterInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\State\Util\StateOptionsTrait; @@ -107,9 +108,20 @@ public function normalize(mixed $data, ?string $format = null, array $context = $resourceClass = $this->getStateOptionsClass($operation, $resourceClass); - if ($currentFilters || ($parameters && \count($parameters))) { - $hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext); - ['mapping' => $mapping, 'keys' => $keys] = $this->getSearchMappingAndKeys($operation, $resourceClass, $currentFilters, $parameters, [$this, 'getFilter']); + $hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext); + ['mapping' => $mapping, 'keys' => $keys] = $this->getSearchMappingAndKeys($operation, $resourceClass, $currentFilters, $parameters, [$this, 'getFilter']); + + $hasQueryParameters = false; + if ($parameters) { + foreach ($parameters as $parameter) { + if ($parameter instanceof QueryParameterInterface) { + $hasQueryParameters = true; + break; + } + } + } + + if ($currentFilters || $keys || $hasQueryParameters) { $normalizedData[$hydraPrefix.'search'] = [ '@type' => $hydraPrefix.'IriTemplate', $hydraPrefix.'template' => \sprintf('%s{?%s}', $requestParts['path'], implode(',', $keys)), diff --git a/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php b/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php index f49e245511..25eeb721cd 100644 --- a/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php +++ b/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php @@ -43,6 +43,11 @@ public function create(string $resourceClass): ResourceMetadataCollection $defaultParams = $this->buildDefaultParameters(); foreach ($resourceMetadataCollection as $i => $resource) { + $originalOperations = []; + foreach ($resource->getOperations() ?? [] as $operationName => $operation) { + $originalOperations[$operationName] = null !== $operation->getParameters(); + } + $resource = $this->applyDefaults($resource, $defaultParams); $operations = $resource->getOperations(); @@ -54,7 +59,7 @@ public function create(string $resourceClass): ResourceMetadataCollection } // As we deprecate the parameter validator, we declare a parameter for each filter transfering validation to the new system - if ($operation->getFilters() && 0 === $parameters->count()) { + if ($operation->getFilters() && !($originalOperations[$operationName] ?? false)) { $parameters = $this->addFilterValidation($operation); } diff --git a/tests/Functional/DefaultParametersAppKernel.php b/tests/Functional/DefaultParametersAppKernel.php new file mode 100644 index 0000000000..b61bdb1c3f --- /dev/null +++ b/tests/Functional/DefaultParametersAppKernel.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Functional; + +use ApiPlatform\Metadata\HeaderParameter; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Maxence Castel + */ +class DefaultParametersAppKernel extends \AppKernel +{ + protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void + { + parent::configureContainer($c, $loader); + + $loader->load(static function (ContainerBuilder $container) { + if ($container->hasDefinition('phpunit_resource_name_collection')) { + $container->removeDefinition('phpunit_resource_name_collection'); + } + + $container->loadFromExtension('api_platform', [ + 'defaults' => [ + 'extra_properties' => [ + 'deduplicate_resource_short_names' => true, + ], + 'parameters' => [ + HeaderParameter::class => [ + 'key' => 'X-API-Key', + 'required' => false, + 'description' => 'API key for authentication', + 'schema' => ['type' => 'string'], + ], + ], + ], + ]); + }); + } +} diff --git a/tests/Functional/DefaultParametersTest.php b/tests/Functional/DefaultParametersTest.php new file mode 100644 index 0000000000..0b0f6698a9 --- /dev/null +++ b/tests/Functional/DefaultParametersTest.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Functional; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; + +/** + * Tests that default parameters configured via api_platform.defaults.parameters + * appear in all resources and operations in the OpenAPI and JSONSchema documentation. + * + * @author Maxence Castel + */ +final class DefaultParametersTest extends ApiTestCase +{ + protected static ?bool $alwaysBootKernel = true; + + protected static function getKernelClass(): string + { + return DefaultParametersAppKernel::class; + } + + /** + * Test that default header parameter appears in all operations in OpenAPI documentation. + * + * This test verifies that when default parameters are configured via + * api_platform.defaults.parameters with: + * HeaderParameter: + * key: 'X-API-Key' + * required: false + * description: 'API key for authentication' + * + * The parameter appears in ALL resources and ALL their operations in the OpenAPI output. + */ + public function testDefaultParameterAppearsInOpenApiForAllOperations(): void + { + $response = self::createClient()->request('GET', '/docs', [ + 'headers' => ['Accept' => 'application/vnd.openapi+json'], + ]); + + $this->assertResponseIsSuccessful(); + $content = $response->toArray(); + + $this->assertArrayHasKey('openapi', $content); + $this->assertArrayHasKey('paths', $content); + + $foundParameter = false; + $operationsWithParameter = []; + + foreach ($content['paths'] as $pathName => $pathItem) { + foreach (['get', 'post', 'put', 'patch', 'delete'] as $method) { + if (!isset($pathItem[$method]['parameters'])) { + continue; + } + + $parameters = $pathItem[$method]['parameters']; + foreach ($parameters as $param) { + if ('X-API-Key' === $param['name'] && 'header' === $param['in']) { + $foundParameter = true; + $operationsWithParameter[] = [ + 'path' => $pathName, + 'method' => $method, + ]; + + $this->assertSame('X-API-Key', $param['name']); + $this->assertSame('header', $param['in']); + $this->assertSame('API key for authentication', $param['description']); + $this->assertFalse($param['required']); + $this->assertFalse($param['deprecated']); + $this->assertArrayHasKey('schema', $param); + $this->assertSame('string', $param['schema']['type']); + break; + } + } + } + } + + $this->assertTrue( + $foundParameter, + \sprintf( + 'Default header parameter "X-API-Key" not found in any operation. Operations checked: %d', + \count($content['paths'] ?? []) + ) + ); + + $this->assertGreaterThanOrEqual(2, \count($operationsWithParameter), + 'Default parameter should appear in multiple operations (collection and item)' + ); + } + + /** + * Test that default parameters appear in both collection and item operations. + */ + public function testDefaultParameterAppearsInMultipleOperationTypes(): void + { + $response = self::createClient()->request('GET', '/docs', [ + 'headers' => ['Accept' => 'application/vnd.openapi+json'], + ]); + + $this->assertResponseIsSuccessful(); + $content = $response->toArray(); + + $operationMethodsWithParameter = []; + + foreach ($content['paths'] as $pathName => $pathItem) { + foreach (['get', 'post', 'put', 'patch', 'delete'] as $method) { + if (!isset($pathItem[$method]['parameters'])) { + continue; + } + + $parameters = $pathItem[$method]['parameters']; + foreach ($parameters as $param) { + if ('X-API-Key' === $param['name'] && 'header' === $param['in']) { + $operationMethodsWithParameter[$method] = true; + break; + } + } + } + } + + $this->assertGreaterThanOrEqual(2, \count($operationMethodsWithParameter), + \sprintf('Default parameter should appear in at least 2 different HTTP methods, found in: %s', + implode(', ', array_keys($operationMethodsWithParameter))) + ); + } + + public function testDefaultParametersDoNotBreakJsonLdDocumentation(): void + { + $response = self::createClient()->request('GET', '/docs.jsonld', [ + 'headers' => ['Accept' => 'application/ld+json'], + ]); + + $this->assertResponseIsSuccessful(); + $content = $response->toArray(); + + $this->assertArrayHasKey('@context', $content); + + $this->assertTrue( + isset($content['entrypoint']) || isset($content['hydra:supportedClass']), + 'JSON-LD response should have either "entrypoint" or "hydra:supportedClass" key' + ); + } +} diff --git a/tests/Functional/JsonSchema/JsonSchemaWithDefaultParametersTest.php b/tests/Functional/JsonSchema/JsonSchemaWithDefaultParametersTest.php new file mode 100644 index 0000000000..efe0c20a2b --- /dev/null +++ b/tests/Functional/JsonSchema/JsonSchemaWithDefaultParametersTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Functional\JsonSchema; + +use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5793\BagOfTests; +use ApiPlatform\Tests\Functional\DefaultParametersAppKernel; + +/** + * Test that JsonSchema can be generated when default parameters are configured. + * + * @author Maxence Castel + */ +final class JsonSchemaWithDefaultParametersTest extends ApiTestCase +{ + protected SchemaFactoryInterface $schemaFactory; + protected OperationMetadataFactoryInterface $operationMetadataFactory; + + protected static ?bool $alwaysBootKernel = true; + + protected static function getKernelClass(): string + { + return DefaultParametersAppKernel::class; + } + + protected function setUp(): void + { + parent::setUp(); + $this->schemaFactory = self::getContainer()->get('api_platform.json_schema.schema_factory'); + $this->operationMetadataFactory = self::getContainer()->get('api_platform.metadata.operation.metadata_factory'); + } + + public function testJsonSchemaCanBeGeneratedWithDefaultParameters(): void + { + $hasDefaultParameters = false; + $resourceMetadata = self::getContainer()->get('api_platform.metadata.resource.metadata_collection_factory')->create(BagOfTests::class); + + foreach ($resourceMetadata as $operation) { + $parameters = $operation->getParameters() ?? []; + foreach ($parameters as $parameter) { + if ('X-API-Key' === $parameter->getKey()) { + $hasDefaultParameters = true; + $this->assertFalse($parameter->getRequired()); + $this->assertSame('API key for authentication', $parameter->getDescription()); + break; + } + } + if ($hasDefaultParameters) { + break; + } + } + + $this->assertTrue($hasDefaultParameters, 'Default parameter "X-API-Key" should be applied to resource operations'); + + $schema = $this->schemaFactory->buildSchema(BagOfTests::class, 'jsonld'); + + $this->assertInstanceOf(\ArrayObject::class, $schema); + + $this->assertArrayHasKey('definitions', $schema); + $this->assertNotEmpty($schema['definitions']); + + foreach ($schema['definitions'] as $key => $definition) { + $this->assertIsString($key); + $this->assertNotNull($definition); + } + } +}