Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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 = [];
Expand Down
13 changes: 13 additions & 0 deletions src/Symfony/Bundle/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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%',
]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand All @@ -30,14 +32,19 @@ final class ParameterValidationResourceMetadataCollectionFactory implements Reso
public function __construct(
private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null,
private readonly ?ContainerInterface $filterLocator = null,
private readonly array $defaultParameters = [],
) {
}

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) {
Expand Down Expand Up @@ -135,4 +142,109 @@ private function addFilterValidation(HttpOperation $operation): Parameters

return $parameters;
}

/**
* Builds Parameter objects from the default configuration array.
*
* @return array<string, Parameter> 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<Parameter> $parameterClass The parameter class name
* @param array<string, mixed> $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<string, Parameter> $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<string, Parameter> $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;
}
}
28 changes: 28 additions & 0 deletions tests/Fixtures/app/AppKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,14 @@ class_exists(NativePasswordHasher::class) ? 'password_hashers' : 'encoders' => [
Patch::class,
Delete::class,
],
'parameters' => [
ApiPlatform\Metadata\HeaderParameter::class => [
'key' => 'X-API-Key',
'required' => false,
'description' => 'API key for authentication',
'schema' => ['type' => 'string'],
],
],
],
'serializer' => [
'hydra_prefix' => true,
Expand Down Expand Up @@ -323,6 +331,16 @@ class_exists(NativePasswordHasher::class) ? 'password_hashers' : 'encoders' => [
'%kernel.project_dir%/../TestBundle/ApiResourceOdm',
],
],
'defaults' => [
'parameters' => [
ApiPlatform\Metadata\HeaderParameter::class => [
'key' => 'X-API-Key',
'required' => false,
'description' => 'API key for authentication',
'schema' => ['type' => 'string'],
],
],
],
]);

return;
Expand All @@ -332,6 +350,16 @@ class_exists(NativePasswordHasher::class) ? 'password_hashers' : 'encoders' => [
'mapping' => [
'paths' => ['%kernel.project_dir%/../TestBundle/Resources/config/api_resources_orm'],
],
'defaults' => [
'parameters' => [
ApiPlatform\Metadata\HeaderParameter::class => [
'key' => 'X-API-Key',
'required' => false,
'description' => 'API key for authentication',
'schema' => ['type' => 'string'],
],
],
],
]);
}

Expand Down
12 changes: 12 additions & 0 deletions tests/Functional/BackedEnumResourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,12 @@ public function testCollectionJsonLd(): void
'value' => 200,
],
],
'hydra:search' => [
'@type' => 'hydra:IriTemplate',
'hydra:template' => '/availabilities{?}',
'hydra:variableRepresentation' => 'BasicRepresentation',
'hydra:mapping' => [],
],
]);
}

Expand Down Expand Up @@ -356,6 +362,12 @@ public static function providerCollection(): iterable
'description' => 'Let me think about it',
],
],
'hydra:search' => [
'@type' => 'hydra:IriTemplate',
'hydra:template' => '/backed_enum_integer_resources{?}',
'hydra:variableRepresentation' => 'BasicRepresentation',
'hydra:mapping' => [],
],
]];

yield 'HAL+JSON' => ['application/hal+json', [
Expand Down
Loading
Loading