diff --git a/src/Hydra/Serializer/CollectionFiltersNormalizer.php b/src/Hydra/Serializer/CollectionFiltersNormalizer.php index 9625e992260..34121f97f2e 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/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 1f2f6689b96..c02a0786c69 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 a3ecfc14078..27cc86a9d4a 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/validator/validator.php b/src/Symfony/Bundle/Resources/config/validator/validator.php index 786221ea971..9372bdb2346 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 27bc2b2dc1e..25eeb721cd4 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; @@ -22,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 { @@ -30,6 +32,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 +40,16 @@ public function create(string $resourceClass): ResourceMetadataCollection { $resourceMetadataCollection = $this->decorated?->create($resourceClass) ?? new ResourceMetadataCollection($resourceClass); + $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(); foreach ($operations as $operationName => $operation) { @@ -47,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); } @@ -135,4 +147,109 @@ 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 + { + $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); + } + + /** + * 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/Functional/DefaultParametersAppKernel.php b/tests/Functional/DefaultParametersAppKernel.php new file mode 100644 index 00000000000..b61bdb1c3f9 --- /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 00000000000..0b0f6698a95 --- /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 00000000000..efe0c20a2b7 --- /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); + } + } +} diff --git a/tests/Validator/Metadata/Resource/Factory/ParameterValidationConfigurationTest.php b/tests/Validator/Metadata/Resource/Factory/ParameterValidationConfigurationTest.php new file mode 100644 index 00000000000..bb434e0d8d6 --- /dev/null +++ b/tests/Validator/Metadata/Resource/Factory/ParameterValidationConfigurationTest.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\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 used by ParameterValidationResourceMetadataCollectionFactory. + * + * @author Maxence Castel + */ +final class ParameterValidationConfigurationTest 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']); + } +} diff --git a/tests/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactoryDefaultParametersTest.php b/tests/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactoryDefaultParametersTest.php new file mode 100644 index 00000000000..9cd0a2bec04 --- /dev/null +++ b/tests/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactoryDefaultParametersTest.php @@ -0,0 +1,133 @@ + + * + * 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\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\Validator\Metadata\Resource\Factory\ParameterValidationResourceMetadataCollectionFactory; +use PHPUnit\Framework\TestCase; + +/** + * Integration tests for ParameterValidationResourceMetadataCollectionFactory with default parameters. + * + * @author Maxence Castel + */ +final class ParameterValidationResourceMetadataCollectionFactoryDefaultParametersTest extends TestCase +{ + private const DEFAULT_PARAMETERS = [ + HeaderParameter::class => [ + 'key' => 'X-API-Version', + 'required' => true, + 'description' => 'API Version', + ], + ]; + + public function testDefaultParametersAppliedToRealResource(): void + { + $attributesFactory = new AttributesResourceMetadataCollectionFactory(); + $parameterValidationFactory = new ParameterValidationResourceMetadataCollectionFactory( + $attributesFactory, + null, + self::DEFAULT_PARAMETERS + ); + + $resourceClass = TestProductResource::class; + + $collection = $parameterValidationFactory->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(); + $parameterValidationFactory = new ParameterValidationResourceMetadataCollectionFactory( + $attributesFactory, + null, + self::DEFAULT_PARAMETERS + ); + + $resourceClass = TestProductResourceWithParameters::class; + + $collection = $parameterValidationFactory->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'; +}