From e6cea1ff274938e7b871cb8c654abcb782b3b33d Mon Sep 17 00:00:00 2001 From: Maxcastel Date: Fri, 13 Feb 2026 14:38:56 +0100 Subject: [PATCH 1/3] test defaults parameters --- .../ApiPlatformExtension.php | 59 ++++++ ...latformExtensionParameterClassNameTest.php | 185 ++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionParameterClassNameTest.php diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 1f2f6689b9..d974d803d0 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -47,6 +47,8 @@ use ApiPlatform\Metadata\McpTool; use ApiPlatform\Metadata\NotExposed; use ApiPlatform\Metadata\OperationMutatorInterface; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\Parameters; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Put; @@ -439,9 +441,66 @@ private function normalizeDefaults(array $defaults): array $normalizedDefaults['extra_properties'][$option] = $value; } + if (isset($normalizedDefaults['parameters']) && \is_array($normalizedDefaults['parameters'])) { + $normalizedDefaults['parameters'] = $this->normalizeParametersConfig($normalizedDefaults['parameters']); + } + return $normalizedDefaults; } + private function normalizeParametersConfig(array $parametersConfig): Parameters|array + { + $parameters = []; + $hasClassNames = false; + + foreach ($parametersConfig as $key => $config) { + if (class_exists($key) && is_subclass_of($key, Parameter::class)) { + $hasClassNames = true; + $parameterClass = $key; + $parameterConfig = \is_array($config) ? $config : []; + + try { + $reflection = new \ReflectionClass($parameterClass); + $constructor = $reflection->getConstructor(); + + if (null === $constructor) { + continue; + } + + $args = []; + foreach ($constructor->getParameters() as $param) { + $paramName = $param->getName(); + if (isset($parameterConfig[$paramName])) { + $args[$paramName] = $parameterConfig[$paramName]; + } elseif ($param->isDefaultValueAvailable()) { + $args[$paramName] = $param->getDefaultValue(); + } + } + + $instance = $reflection->newInstance(...$args); + + if (null !== $instance->getKey()) { + $parameters[$instance->getKey()] = $instance; + } + } catch (\ReflectionException|\TypeError $e) { + continue; + } + } else { + $parameters[$key] = $config; + } + } + + if ($hasClassNames || !empty($parameters)) { + try { + return new Parameters($parameters); + } catch (\Throwable $e) { + return $parameters; + } + } + + return $parameters; + } + private function registerMetadataConfiguration(ContainerBuilder $container, array $config, PhpFileLoader $loader): void { [$xmlResources, $yamlResources, $phpResources] = $this->getResourcesToWatch($container, $config); diff --git a/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionParameterClassNameTest.php b/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionParameterClassNameTest.php new file mode 100644 index 0000000000..491511c8f9 --- /dev/null +++ b/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionParameterClassNameTest.php @@ -0,0 +1,185 @@ + + * + * 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\Symfony\Tests\Bundle\DependencyInjection; + +use ApiPlatform\Metadata\HeaderParameter; +use ApiPlatform\Metadata\Parameters; +// use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\Symfony\Bundle\DependencyInjection\ApiPlatformExtension; +use ApiPlatform\Tests\Fixtures\TestBundle\TestBundle; +use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\TwigBundle\TwigBundle; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; + +/** + * Test that parameters can be defined using Parameter class names as keys. + * + * Example: + * ```yaml + * defaults: + * parameters: + * 'ApiPlatform\Metadata\HeaderParameter': + * key: 'X-Api-Version' + * required: true + * ``` + */ +class ApiPlatformExtensionParameterClassNameTest extends TestCase +{ + private ContainerBuilder $container; + + protected function setUp(): void + { + $containerParameterBag = new ParameterBag([ + 'kernel.bundles' => [ + 'DoctrineBundle' => DoctrineBundle::class, + 'SecurityBundle' => SecurityBundle::class, + 'TwigBundle' => TwigBundle::class, + ], + 'kernel.bundles_metadata' => [ + 'TestBundle' => [ + 'parent' => null, + 'path' => realpath(__DIR__.'/../../../Fixtures/TestBundle'), + 'namespace' => TestBundle::class, + ], + ], + 'kernel.project_dir' => __DIR__.'/../../../Fixtures/app', + 'kernel.debug' => false, + 'kernel.environment' => 'test', + ]); + + $this->container = new ContainerBuilder($containerParameterBag); + } + + public function testParametersWithClassNameAsKey(): void + { + $config = [ + 'api_platform' => [ + 'title' => 'Test API', + 'description' => 'Test Description', + 'version' => '1.0.0', + 'formats' => ['json' => ['mime_types' => ['application/json']]], + 'error_formats' => [], + 'patch_formats' => [], + 'defaults' => [ + 'parameters' => [ + 'ApiPlatform\Metadata\HeaderParameter' => [ + 'key' => 'X-Api-Version', + 'required' => true, + 'description' => 'API Version', + ], + // 'ApiPlatform\Metadata\QueryParameter' => [ + // 'key' => 'q', + // 'description' => 'Search query', + // ], + ], + ], + ], + ]; + + (new ApiPlatformExtension())->load($config, $this->container); + + $defaults = $this->container->getParameter('api_platform.defaults'); + $this->assertArrayHasKey('parameters', $defaults); + + /** @var Parameters $parameters */ + $parameters = $defaults['parameters']; + $this->assertInstanceOf(Parameters::class, $parameters); + + $paramArray = iterator_to_array($parameters); + $this->assertNotEmpty($paramArray); + + $this->assertArrayHasKey('X-Api-Version', $paramArray); + $headerParam = $paramArray['X-Api-Version']; + $this->assertInstanceOf(HeaderParameter::class, $headerParam); + $this->assertTrue($headerParam->getRequired()); + + // $this->assertArrayHasKey('q', $paramArray); + // $queryParam = $paramArray['q']; + // $this->assertInstanceOf(QueryParameter::class, $queryParam); + } + + public function testMixedParameterDefinitions(): void + { + $config = [ + 'api_platform' => [ + 'title' => 'Test API', + 'description' => 'Test Description', + 'version' => '1.0.0', + 'formats' => ['json' => ['mime_types' => ['application/json']]], + 'error_formats' => [], + 'patch_formats' => [], + 'defaults' => [ + 'parameters' => [ + 'ApiPlatform\Metadata\HeaderParameter' => [ + 'key' => 'X-Api-Version', + 'required' => true, + ], + // 'ApiPlatform\Metadata\QueryParameter' => [ + // 'key' => 'q', + // 'description' => 'Search query', + // ], + ], + ], + ], + ]; + + (new ApiPlatformExtension())->load($config, $this->container); + + $defaults = $this->container->getParameter('api_platform.defaults'); + /** @var Parameters $parameters */ + $parameters = $defaults['parameters']; + + $paramArray = iterator_to_array($parameters); + + $this->assertArrayHasKey('X-Api-Version', $paramArray); + $this->assertInstanceOf(HeaderParameter::class, $paramArray['X-Api-Version']); + + // $this->assertArrayHasKey('q', $paramArray); + // $this->assertInstanceOf(QueryParameter::class, $paramArray['q']); + } + + public function testMultipleHeaderParameters(): void + { + $config = [ + 'api_platform' => [ + 'title' => 'Test API', + 'description' => 'Test Description', + 'version' => '1.0.0', + 'formats' => ['json' => ['mime_types' => ['application/json']]], + 'error_formats' => [], + 'patch_formats' => [], + 'defaults' => [ + 'parameters' => [ + 'ApiPlatform\Metadata\HeaderParameter' => [ + 'key' => 'X-Api-Version', + 'required' => true, + ], + ], + ], + ], + ]; + + (new ApiPlatformExtension())->load($config, $this->container); + + $defaults = $this->container->getParameter('api_platform.defaults'); + /** @var Parameters $parameters */ + $parameters = $defaults['parameters']; + + $paramArray = iterator_to_array($parameters); + $this->assertNotEmpty($paramArray); + } +} From 4e60ee0c70aac5bd45692e1dc2d6ebd52f15bee4 Mon Sep 17 00:00:00 2001 From: Maxcastel Date: Fri, 13 Feb 2026 14:57:27 +0100 Subject: [PATCH 2/3] fix phpstan --- ...latformExtensionParameterClassNameTest.php | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionParameterClassNameTest.php b/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionParameterClassNameTest.php index 491511c8f9..e5bb1ff256 100644 --- a/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionParameterClassNameTest.php +++ b/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionParameterClassNameTest.php @@ -68,12 +68,6 @@ public function testParametersWithClassNameAsKey(): void { $config = [ 'api_platform' => [ - 'title' => 'Test API', - 'description' => 'Test Description', - 'version' => '1.0.0', - 'formats' => ['json' => ['mime_types' => ['application/json']]], - 'error_formats' => [], - 'patch_formats' => [], 'defaults' => [ 'parameters' => [ 'ApiPlatform\Metadata\HeaderParameter' => [ @@ -95,7 +89,6 @@ public function testParametersWithClassNameAsKey(): void $defaults = $this->container->getParameter('api_platform.defaults'); $this->assertArrayHasKey('parameters', $defaults); - /** @var Parameters $parameters */ $parameters = $defaults['parameters']; $this->assertInstanceOf(Parameters::class, $parameters); @@ -116,12 +109,6 @@ public function testMixedParameterDefinitions(): void { $config = [ 'api_platform' => [ - 'title' => 'Test API', - 'description' => 'Test Description', - 'version' => '1.0.0', - 'formats' => ['json' => ['mime_types' => ['application/json']]], - 'error_formats' => [], - 'patch_formats' => [], 'defaults' => [ 'parameters' => [ 'ApiPlatform\Metadata\HeaderParameter' => [ @@ -140,8 +127,10 @@ public function testMixedParameterDefinitions(): void (new ApiPlatformExtension())->load($config, $this->container); $defaults = $this->container->getParameter('api_platform.defaults'); - /** @var Parameters $parameters */ + $this->assertArrayHasKey('parameters', $defaults); + $parameters = $defaults['parameters']; + $this->assertInstanceOf(Parameters::class, $parameters); $paramArray = iterator_to_array($parameters); @@ -156,12 +145,6 @@ public function testMultipleHeaderParameters(): void { $config = [ 'api_platform' => [ - 'title' => 'Test API', - 'description' => 'Test Description', - 'version' => '1.0.0', - 'formats' => ['json' => ['mime_types' => ['application/json']]], - 'error_formats' => [], - 'patch_formats' => [], 'defaults' => [ 'parameters' => [ 'ApiPlatform\Metadata\HeaderParameter' => [ @@ -176,8 +159,10 @@ public function testMultipleHeaderParameters(): void (new ApiPlatformExtension())->load($config, $this->container); $defaults = $this->container->getParameter('api_platform.defaults'); - /** @var Parameters $parameters */ + $this->assertArrayHasKey('parameters', $defaults); + $parameters = $defaults['parameters']; + $this->assertInstanceOf(Parameters::class, $parameters); $paramArray = iterator_to_array($parameters); $this->assertNotEmpty($paramArray); From 4cf03bca6c8c8db9d4287322ac38f55b27db26e8 Mon Sep 17 00:00:00 2001 From: Maxcastel Date: Fri, 13 Feb 2026 16:42:25 +0100 Subject: [PATCH 3/3] test OpenApi --- tests/Fixtures/TestBundle/Entity/Book.php | 3 +- tests/Fixtures/app/config/config_common.yml | 5 + .../GlobalDefaultsParametersOpenApiTest.php | 92 +++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 tests/Functional/OpenApi/GlobalDefaultsParametersOpenApiTest.php diff --git a/tests/Fixtures/TestBundle/Entity/Book.php b/tests/Fixtures/TestBundle/Entity/Book.php index f892b81cb3..17adf7f380 100644 --- a/tests/Fixtures/TestBundle/Entity/Book.php +++ b/tests/Fixtures/TestBundle/Entity/Book.php @@ -15,6 +15,7 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; use Doctrine\ORM\Mapping as ORM; /** @@ -22,7 +23,7 @@ * * @author Antoine Bluchet */ -#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}{._format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] +#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}{._format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn'), new GetCollection()])] #[ORM\Entity] class Book { diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 00f270c821..9dd671ff88 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -38,6 +38,11 @@ api_platform: Made with love enable_swagger: true enable_swagger_ui: true + defaults: + parameters: + 'ApiPlatform\Metadata\HeaderParameter': + key: 'X-Request-ID' + description: 'A unique request identifier' formats: jsonld: ['application/ld+json'] jsonhal: ['application/hal+json'] diff --git a/tests/Functional/OpenApi/GlobalDefaultsParametersOpenApiTest.php b/tests/Functional/OpenApi/GlobalDefaultsParametersOpenApiTest.php new file mode 100644 index 0000000000..7e6790d12a --- /dev/null +++ b/tests/Functional/OpenApi/GlobalDefaultsParametersOpenApiTest.php @@ -0,0 +1,92 @@ + + * + * 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\OpenApi; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Book; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\RecreateSchemaTrait; +use ApiPlatform\Tests\SetupClassResourcesTrait; + +/** + * Test that global default parameters are applied to ALL resources in OpenAPI spec. + * + * When parameters are defined in api_platform.defaults.parameters with class names as keys, + * they should appear in the OpenAPI documentation for EVERY resource, not just specific ones. + */ +class GlobalDefaultsParametersOpenApiTest extends ApiTestCase +{ + use RecreateSchemaTrait; + use SetupClassResourcesTrait; + + protected static ?bool $alwaysBootKernel = false; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [ + Book::class, + Dummy::class, + ]; + } + + /** + * Test that global HeaderParameter with description appears in schema. + * + * If we configured global parameters like: + * ```yaml + * defaults: + * parameters: + * 'ApiPlatform\Metadata\HeaderParameter': + * description: 'A unique request identifier' + * ``` + * + * Then this parameter should appear in OpenAPI for all resources. + */ + public function testGlobalHeaderParameterAppearsInSchema(): void + { + $globalHeaderParameterDescription = 'A unique request identifier'; + $globalHeaderParameterKey = 'X-Request-ID'; + + $bookResponse = self::createClient()->request('GET', '/docs', [ + 'headers' => ['Accept' => 'application/vnd.openapi+json'], + ]); + + $this->assertResponseIsSuccessful(); + $bookRes = $bookResponse->toArray(); + + $bookParameters = $bookRes['paths']['/books']['get']['parameters']; + $this->assertTrue(isset($bookParameters)); + + $bookParametersHeader = $bookParameters[4]; + $this->assertSame($globalHeaderParameterDescription, $bookParametersHeader['description']); + $this->assertSame($globalHeaderParameterKey, $bookParametersHeader['name']); + + $dummyResponse = self::createClient()->request('GET', '/docs', [ + 'headers' => ['Accept' => 'application/vnd.openapi+json'], + ]); + + $this->assertResponseIsSuccessful(); + $dummyRes = $dummyResponse->toArray(); + + $dummyParameters = $dummyRes['paths']['/dummies/{id}']['get']['parameters']; + $this->assertTrue(isset($dummyParameters)); + + $dummyParametersHeader = $dummyParameters[1]; + $this->assertSame($globalHeaderParameterDescription, $dummyParametersHeader['description']); + $this->assertSame($globalHeaderParameterKey, $dummyParametersHeader['name']); + } +}