From c11e889791976a4d872e0f595e5126187ff6c025 Mon Sep 17 00:00:00 2001 From: soyuka Date: Mon, 4 Mar 2024 15:59:22 +0100 Subject: [PATCH] feat: parameter implementation --- ...> 0006-filtering-system-and-parameters.md} | 2 +- docs/adr/filter-notes | 61 +++++++++ src/Metadata/GraphQl/Subscription.php | 2 +- src/Metadata/HeaderParameter.php | 18 +++ src/Metadata/HeaderParameterInterface.php | 18 +++ src/Metadata/Metadata.php | 18 +-- src/Metadata/Operation.php | 24 ++-- src/Metadata/Parameter.php | 117 ++++++++++++++++-- src/Metadata/Post.php | 1 + src/Metadata/QueryParameter.php | 18 +++ src/Metadata/QueryParameterInterface.php | 18 +++ ...meterResourceMetadataCollectionFactory.php | 103 +++++++++++++++ src/Serializer/Parameter/GroupParameter.php | 0 .../SerializerFilterParameterProvider.php | 61 +++++++++ src/State/ParameterProviderInterface.php | 31 +++++ src/State/Provider/ParameterProvider.php | 79 ++++++++++++ src/Symfony/Bundle/Resources/config/api.xml | 7 ++ .../Resources/config/metadata/resource.xml | 5 + .../Resources/config/state/provider.xml | 5 + .../TestBundle/ApiResource/WithParameter.php | 58 +++++++++ .../CustomGroupParameterProvider.php | 26 ++++ tests/Fixtures/app/config/config_common.yml | 5 + tests/Parameter/ParameterTests.php | 51 ++++++++ 23 files changed, 698 insertions(+), 30 deletions(-) rename docs/adr/{0006-filters.md => 0006-filtering-system-and-parameters.md} (98%) create mode 100644 docs/adr/filter-notes create mode 100644 src/Metadata/HeaderParameter.php create mode 100644 src/Metadata/HeaderParameterInterface.php create mode 100644 src/Metadata/QueryParameter.php create mode 100644 src/Metadata/QueryParameterInterface.php create mode 100644 src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php create mode 100644 src/Serializer/Parameter/GroupParameter.php create mode 100644 src/Serializer/Parameter/SerializerFilterParameterProvider.php create mode 100644 src/State/ParameterProviderInterface.php create mode 100644 src/State/Provider/ParameterProvider.php create mode 100644 tests/Fixtures/TestBundle/ApiResource/WithParameter.php create mode 100644 tests/Fixtures/TestBundle/Parameter/CustomGroupParameterProvider.php create mode 100644 tests/Parameter/ParameterTests.php diff --git a/docs/adr/0006-filters.md b/docs/adr/0006-filtering-system-and-parameters.md similarity index 98% rename from docs/adr/0006-filters.md rename to docs/adr/0006-filtering-system-and-parameters.md index c5e7e4882b9..7b37456126d 100644 --- a/docs/adr/0006-filters.md +++ b/docs/adr/0006-filtering-system-and-parameters.md @@ -71,7 +71,7 @@ For this to work, we need to consider a 4 year old bug on searching with UIDs. O /books?author.id=/author/1 ``` -Many attempts to fix these behavior on API Platform have lead to bugs and to be reverted. My proposal is to change how filters are applied to provide filters with less logic, that are easier to maintain and that do one thing good. +Many attempts to fix these behavior on API Platform have lead to bugs and to be reverted. The proposal is to change how filters are applied to provide filters with less logic, that are easier to maintain and that do one thing good. For the following example we will use an UUID to represent the stored identifier of an Author resource. diff --git a/docs/adr/filter-notes b/docs/adr/filter-notes new file mode 100644 index 00000000000..8b700fbe383 --- /dev/null +++ b/docs/adr/filter-notes @@ -0,0 +1,61 @@ +```php +// how to give uidfilter the parameters it should declare? +// is it automatic if we find a property having the uid type? +#[Get(filters: [new SearchFilter(), new UidFilter()]) +#[Parameter('key', schema: ['type' => 'string'])] // add transform + validate extension points +class Book { + +} + +final class Parameter { + mixed $value; + ?string $property; + ?string $class; + array $attributes; +} + +class FilterInterface {} + +class UidFilter { + public function __construct(private readonly string $class) {} + + public function parseQueryParameter(array $queryParameters = []): Parameter[] { + return [ + new Parameter(value: '', attributes: ['operation' => 'and']) + ]; + } + + // Query parameter type + public function getSchema(): array { + return ['type' => 'string']; + } + + public function getOpenApiParameter(): OpenApi\Parameter { + return ...; + } +} + +public function process(Operation $operation) { + $request = $context['request']; + + foreach($operation->getFilters() as $filter) { + foreach ($filter->parseQueryParameter($request->query, $context) as $parameter) { + $this->queryParameterValidator->validate($filter, $parameter, $context); + $filter->execute($filter, $parameter, $context); + } + } +} +``` + + +TODO: +see SerializerFilterContextBuilder: public function apply(Request $request, bool $normalization, array $attributes, array &$context): void; +maybe something like: + +``` +class SerializerFilterInterface { + public function getNormalizationContext(...); + public function getDenormalizationContext(...); +} +``` + diff --git a/src/Metadata/GraphQl/Subscription.php b/src/Metadata/GraphQl/Subscription.php index ae1017748d0..3e096304635 100644 --- a/src/Metadata/GraphQl/Subscription.php +++ b/src/Metadata/GraphQl/Subscription.php @@ -17,7 +17,7 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] final class Subscription extends Operation - +{ public function __construct( ?string $resolver = null, ?array $args = null, diff --git a/src/Metadata/HeaderParameter.php b/src/Metadata/HeaderParameter.php new file mode 100644 index 00000000000..b12be70e0cb --- /dev/null +++ b/src/Metadata/HeaderParameter.php @@ -0,0 +1,18 @@ + + * + * 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; + +class HeaderParameter extends Parameter implements HeaderParameterInterface +{ +} diff --git a/src/Metadata/HeaderParameterInterface.php b/src/Metadata/HeaderParameterInterface.php new file mode 100644 index 00000000000..4ecc8404b29 --- /dev/null +++ b/src/Metadata/HeaderParameterInterface.php @@ -0,0 +1,18 @@ + + * + * 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; + +interface HeaderParameterInterface +{ +} diff --git a/src/Metadata/Metadata.php b/src/Metadata/Metadata.php index 725a94bc5f8..d189375dd80 100644 --- a/src/Metadata/Metadata.php +++ b/src/Metadata/Metadata.php @@ -21,15 +21,15 @@ abstract class Metadata { /** - * @param string|null $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties - * @param string|null $security https://api-platform.com/docs/core/security - * @param string|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization - * @param mixed|null $mercure - * @param mixed|null $messenger - * @param mixed|null $input - * @param mixed|null $output - * @param mixed|null $provider - * @param mixed|null $processor + * @param string|null $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties + * @param string|null $security https://api-platform.com/docs/core/security + * @param string|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization + * @param mixed|null $mercure + * @param mixed|null $messenger + * @param mixed|null $input + * @param mixed|null $output + * @param mixed|null $provider + * @param mixed|null $processor * @param array $parameters */ public function __construct( diff --git a/src/Metadata/Operation.php b/src/Metadata/Operation.php index c35ae0fb6f9..79efbe772c6 100644 --- a/src/Metadata/Operation.php +++ b/src/Metadata/Operation.php @@ -47,18 +47,18 @@ abstract class Operation extends Metadata * class?: string|null, * name?: string, * }|string|false|null $output {@see https://api-platform.com/docs/core/dto/#specifying-an-input-or-an-output-data-representation} - * @param string|array|bool|null $mercure {@see https://api-platform.com/docs/core/mercure} - * @param string|bool|null $messenger {@see https://api-platform.com/docs/core/messenger/#dispatching-a-resource-through-the-message-bus} - * @param bool|null $elasticsearch {@see https://api-platform.com/docs/core/elasticsearch/} - * @param bool|null $read {@see https://api-platform.com/docs/core/events/#the-event-system} - * @param bool|null $deserialize {@see https://api-platform.com/docs/core/events/#the-event-system} - * @param bool|null $validate {@see https://api-platform.com/docs/core/events/#the-event-system} - * @param bool|null $write {@see https://api-platform.com/docs/core/events/#the-event-system} - * @param bool|null $serialize {@see https://api-platform.com/docs/core/events/#the-event-system} - * @param bool|null $fetchPartial {@see https://api-platform.com/docs/core/performance/#fetch-partial} - * @param bool|null $forceEager {@see https://api-platform.com/docs/core/performance/#force-eager} - * @param string|callable|null $provider {@see https://api-platform.com/docs/core/state-providers/#state-providers} - * @param string|callable|null $processor {@see https://api-platform.com/docs/core/state-processors/#state-processors} + * @param string|array|bool|null $mercure {@see https://api-platform.com/docs/core/mercure} + * @param string|bool|null $messenger {@see https://api-platform.com/docs/core/messenger/#dispatching-a-resource-through-the-message-bus} + * @param bool|null $elasticsearch {@see https://api-platform.com/docs/core/elasticsearch/} + * @param bool|null $read {@see https://api-platform.com/docs/core/events/#the-event-system} + * @param bool|null $deserialize {@see https://api-platform.com/docs/core/events/#the-event-system} + * @param bool|null $validate {@see https://api-platform.com/docs/core/events/#the-event-system} + * @param bool|null $write {@see https://api-platform.com/docs/core/events/#the-event-system} + * @param bool|null $serialize {@see https://api-platform.com/docs/core/events/#the-event-system} + * @param bool|null $fetchPartial {@see https://api-platform.com/docs/core/performance/#fetch-partial} + * @param bool|null $forceEager {@see https://api-platform.com/docs/core/performance/#force-eager} + * @param string|callable|null $provider {@see https://api-platform.com/docs/core/state-providers/#state-providers} + * @param string|callable|null $processor {@see https://api-platform.com/docs/core/state-processors/#state-processors} * @param array $parameters */ public function __construct( diff --git a/src/Metadata/Parameter.php b/src/Metadata/Parameter.php index 9ed708918b4..67938d26ca5 100644 --- a/src/Metadata/Parameter.php +++ b/src/Metadata/Parameter.php @@ -1,16 +1,119 @@ + * + * 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; use ApiPlatform\OpenApi; +use ApiPlatform\State\ProviderInterface; + +abstract class Parameter +{ + /** + * @param array $extraProperties + * @param ProviderInterface|string|null $provider + * @param FilterInterface|string|null $filter + */ + public function __construct( + protected ?string $key = null, + protected ?\ArrayObject $schema = null, + protected ?OpenApi\Model\Parameter $openApi = null, + protected mixed $provider = null, + protected mixed $filter = null, + protected array $extraProperties = [], + ) { + } + + public function getKey(): ?string + { + return $this->key; + } + + public function getSchema(): ?\ArrayObject + { + return $this->schema; + } + + public function getOpenApi(): ?OpenApi\Model\Parameter + { + return $this->openApi; + } + + public function getProvider(): mixed + { + return $this->provider; + } + + public function getFilter(): mixed + { + return $this->filter; + } + + public function getExtraProperties(): ?array + { + return $this->extraProperties; + } + + public function withKey(?string $key): static + { + $self = clone $this; + $self->key = $key; + + return $self; + } + + /** + * @param \ArrayObject $schema + */ + public function withSchema(\ArrayObject $schema): static + { + $self = clone $this; + $self->schema = $schema; + + return $self; + } + + public function withOpenApi(OpenApi\Model\Parameter $openApi): static + { + $self = clone $this; + $self->openApi = $openApi; + + return $self; + } + + public function withProvider(mixed $provider): static + { + $self = clone $this; + $self->provider = $provider; + + return $self; + } + + public function withFilter(mixed $filter): static + { + $self = clone $this; + $self->filter = $filter; + + return $self; + } -final class Parameter { - public string $key; - public \ArrayObject $schema; - public array $context; - public ?OpenApi\Model\Parameter $openApi; /** - * @param fn(mixed $value, Parameter $parameter, array $context)|string|null + * @param array $extraProperties */ - public mixed $provider; + public function withExtraProperties(array $extraProperties = []): static + { + $self = clone $this; + $self->extraProperties = $extraProperties; + + return $self; + } } diff --git a/src/Metadata/Post.php b/src/Metadata/Post.php index 689452b503b..ba6d9dc1701 100644 --- a/src/Metadata/Post.php +++ b/src/Metadata/Post.php @@ -171,6 +171,7 @@ class: $class, provider: $provider, processor: $processor, stateOptions: $stateOptions, + parameters: $parameters, extraProperties: $extraProperties ); } diff --git a/src/Metadata/QueryParameter.php b/src/Metadata/QueryParameter.php new file mode 100644 index 00000000000..036f4c244ef --- /dev/null +++ b/src/Metadata/QueryParameter.php @@ -0,0 +1,18 @@ + + * + * 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; + +class QueryParameter extends Parameter implements QueryParameterInterface +{ +} diff --git a/src/Metadata/QueryParameterInterface.php b/src/Metadata/QueryParameterInterface.php new file mode 100644 index 00000000000..3f32c078fe8 --- /dev/null +++ b/src/Metadata/QueryParameterInterface.php @@ -0,0 +1,18 @@ + + * + * 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; + +interface QueryParameterInterface +{ +} diff --git a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php new file mode 100644 index 00000000000..fb49906e330 --- /dev/null +++ b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php @@ -0,0 +1,103 @@ + + * + * 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\FilterInterface; +use ApiPlatform\Metadata\HeaderParameterInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\OpenApi; +use ApiPlatform\Serializer\Filter\FilterInterface as SerializerFilterInterface; +use Psr\Container\ContainerInterface; + +final class ParameterResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface +{ + public function __construct(private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, private readonly ?ContainerInterface $filterLocator = null) + { + } + + public function create(string $resourceClass): ResourceMetadataCollection + { + $resourceMetadataCollection = $this->decorated?->create($resourceClass) ?? new ResourceMetadataCollection($resourceClass); + + foreach ($resourceMetadataCollection as $i => $resource) { + $operations = $resource->getOperations(); + + foreach ($operations as $operationName => $operation) { + $parameters = []; + foreach ($operation->getParameters() ?? [] as $key => $parameter) { + if (null === $parameter->getKey()) { + $parameter = $parameter->withKey($key); + } + + $filter = $parameter->getFilter(); + if (\is_string($filter) && $this->filterLocator->has($filter)) { + $filter = $this->filterLocator->get($filter); + } + + if (!$filter instanceof FilterInterface) { + $parameters[$key] = $parameter; + continue; + } + + if ($filter instanceof SerializerFilterInterface && null === $parameter->getProvider()) { + $parameter = $parameter->withProvider('api_platform.serializer.filter_parameter_provider'); + } + + // Read filter description to populate the Parameter + $description = $filter->getDescription($resourceClass); + if (($schema = $description['schema'] ?? []) && null === $parameter->getSchema()) { + $parameter = $parameter->withSchema($schema); + } + + if (!($openApi = $description['openapi'] ?? null) && null === $parameter->getOpenApi()) { + $parameters[$key] = $parameter; + continue; + } + + if ($openApi instanceof OpenApi\Model\Parameter) { + $parameter = $parameter->withOpenApi($openApi); + $parameters[$key] = $parameter; + continue; + } + + if (\is_array($openApi)) { + $parameters[] = new OpenApi\Model\Parameter( + $key, + $parameter instanceof HeaderParameterInterface ? 'header' : 'query', + $description['description'] ?? '', + $description['required'] ?? $openApi['required'] ?? false, + $openApi['deprecated'] ?? false, + $openApi['allowEmptyValue'] ?? true, + $schema, + $openApi['style'] ?? null, + $openApi['explode'] ?? ('array' === ($schema['type'] ?? null)), + $openApi['allowReserved'] ?? false, + $openApi['example'] ?? null, + isset($openApi['examples'] + ) ? new \ArrayObject($openApi['examples']) : null + ); + } + + $parameters[$key] = $parameter; + } + + $operations->add($operationName, $operation->withParameters($parameters)); + } + + $resourceMetadataCollection[$i] = $resource->withOperations($operations->sort()); + } + + return $resourceMetadataCollection; + } +} diff --git a/src/Serializer/Parameter/GroupParameter.php b/src/Serializer/Parameter/GroupParameter.php new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Serializer/Parameter/SerializerFilterParameterProvider.php b/src/Serializer/Parameter/SerializerFilterParameterProvider.php new file mode 100644 index 00000000000..df174e8760a --- /dev/null +++ b/src/Serializer/Parameter/SerializerFilterParameterProvider.php @@ -0,0 +1,61 @@ + + * + * 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\Serializer\Parameter; + +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Serializer\Filter\FilterInterface; +use ApiPlatform\State\ParameterProviderInterface; +use ApiPlatform\State\Util\RequestAttributesExtractor; +use Psr\Container\ContainerInterface; + +final class SerializerFilterParameterProvider implements ParameterProviderInterface +{ + public function __construct(private readonly ?ContainerInterface $filterLocator) + { + } + + public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?HttpOperation + { + if (null === ($request = $context['request'] ?? null) || null === ($operation = $context['operation'] ?? null)) { + return null; + } + + $filter = $parameter->getFilter(); + if (\is_string($filter)) { + $filter = $this->getFilter($filter); + } + + if ($filter instanceof FilterInterface) { + $context = $operation->getNormalizationContext(); + $filter->apply($request, true, RequestAttributesExtractor::extractAttributes($request), $context); + + return $operation->withNormalizationContext($context); + } + + return null; + } + + /** + * Gets a filter with a backward compatibility. + */ + private function getFilter(string $filterId): ?FilterInterface + { + if ($this->filterLocator && $this->filterLocator->has($filterId)) { + return $this->filterLocator->get($filterId); + } + + return null; + } +} diff --git a/src/State/ParameterProviderInterface.php b/src/State/ParameterProviderInterface.php new file mode 100644 index 00000000000..5c105c7be83 --- /dev/null +++ b/src/State/ParameterProviderInterface.php @@ -0,0 +1,31 @@ + + * + * 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\State; + +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Parameter; + +/** + * Optionnaly transforms request parameters and provides modification to the current Operation. + * + * @experimental + */ +interface ParameterProviderInterface +{ + /** + * @param array $parameters + * @param array|array{request?: Request, resource_class?: string, operation: HttpOperation} $context + */ + public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?HttpOperation; +} diff --git a/src/State/Provider/ParameterProvider.php b/src/State/Provider/ParameterProvider.php new file mode 100644 index 00000000000..b99f97e2247 --- /dev/null +++ b/src/State/Provider/ParameterProvider.php @@ -0,0 +1,79 @@ + + * + * 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\State\Provider; + +use ApiPlatform\Metadata\HeaderParameterInterface; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\Exception\ProviderNotFoundException; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\Util\RequestParser; +use Psr\Container\ContainerInterface; + +class ParameterProvider implements ProviderInterface +{ + public function __construct(private readonly ?ProviderInterface $decorated = null, private readonly ?ContainerInterface $locator = null) + { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + if (null === ($request = $context['request'])) { + return $this->decorated?->provide($operation, $uriVariables, $context); + } + + if (null === $request->attributes->get('_api_query_parameters')) { + $queryString = RequestParser::getQueryString($request); + $request->attributes->set('_api_query_parameters', $queryString ? RequestParser::parseRequestParams($queryString) : []); + } + + if (null === $request->attributes->get('_api_header_parameters')) { + $request->attributes->set('_api_header_parameters', $request->headers->all()); + } + + $context = ['operation' => $operation] + $context; + + foreach ($operation->getParameters() ?? [] as $key => $parameter) { + if (null === ($provider = $parameter->getProvider())) { + continue; + } + + $parameters = $parameter instanceof HeaderParameterInterface ? $request->attributes->get('_api_header_parameters') : $request->attributes->get('_api_query_parameters'); + if (!isset($parameters[$key])) { + continue; + } + + if (\is_callable($provider) && (($op = $provider($parameter, $parameters, $context)) instanceof HttpOperation)) { + $operation = $op; + $request->attributes->set('_api_operation', $operation); + $context['operation'] = $operation; + continue; + } + + if (!\is_string($provider) || !$this->locator->has($provider)) { + throw new ProviderNotFoundException(sprintf('Provider "%s" not found on operation "%s"', $provider, $operation->getName())); + } + + /** @var ProviderInterface $providerInstance */ + $providerInstance = $this->locator->get($provider); + if (($op = $providerInstance->provide($parameter, $parameters, $context)) instanceof HttpOperation) { + $operation = $op; + $request->attributes->set('_api_operation', $operation); + $context['operation'] = $operation; + } + } + + return $this->decorated?->provide($operation, $uriVariables, $context); + } +} diff --git a/src/Symfony/Bundle/Resources/config/api.xml b/src/Symfony/Bundle/Resources/config/api.xml index 7efa4318d10..01474ec58b1 100644 --- a/src/Symfony/Bundle/Resources/config/api.xml +++ b/src/Symfony/Bundle/Resources/config/api.xml @@ -36,6 +36,13 @@ + + + + + + + diff --git a/src/Symfony/Bundle/Resources/config/metadata/resource.xml b/src/Symfony/Bundle/Resources/config/metadata/resource.xml index 55fc4c58a7a..04d8fa2016f 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/resource.xml +++ b/src/Symfony/Bundle/Resources/config/metadata/resource.xml @@ -74,6 +74,11 @@ + + + + + diff --git a/src/Symfony/Bundle/Resources/config/state/provider.xml b/src/Symfony/Bundle/Resources/config/state/provider.xml index 56a42f70f87..d849872a182 100644 --- a/src/Symfony/Bundle/Resources/config/state/provider.xml +++ b/src/Symfony/Bundle/Resources/config/state/provider.xml @@ -25,6 +25,11 @@ + + + + + api_platform.symfony.main_controller diff --git a/tests/Fixtures/TestBundle/ApiResource/WithParameter.php b/tests/Fixtures/TestBundle/ApiResource/WithParameter.php new file mode 100644 index 00000000000..d0a7a3f0da0 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/WithParameter.php @@ -0,0 +1,58 @@ + + * + * 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\Fixtures\TestBundle\ApiResource; + +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\HeaderParameter; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\Serializer\Filter\GroupFilter; +use ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\Serializer\Attribute\Groups; + +#[Get( + parameters: [ + 'groups' => new QueryParameter(filter: new GroupFilter(parameterName: 'groups', overrideDefaultGroups: false)), + 'group' => new QueryParameter(provider: [self::class, 'provideGroup']), + 'properties' => new QueryParameter(filter: 'my_dummy.property'), + 'service' => new QueryParameter(provider: CustomGroupParameterProvider::class), + 'auth' => new HeaderParameter(provider: [self::class, 'restrictAccess']), + ], + provider: [WithParameter::class, 'provide'] +)] +class WithParameter +{ + #[Groups(['a'])] + public $a = 'foo'; + #[Groups(['b', 'custom'])] + public $b = 'bar'; + + public static function provide() + { + return new self(); + } + + public static function provideGroup(Parameter $parameter, array $parameters = [], array $context = []) + { + $operation = $context['operation']; + + return $operation->withNormalizationContext(['groups' => $parameters['group']]); + } + + public static function restrictAccess(): void + { + throw new AccessDeniedHttpException(); + } +} diff --git a/tests/Fixtures/TestBundle/Parameter/CustomGroupParameterProvider.php b/tests/Fixtures/TestBundle/Parameter/CustomGroupParameterProvider.php new file mode 100644 index 00000000000..35b0f88817b --- /dev/null +++ b/tests/Fixtures/TestBundle/Parameter/CustomGroupParameterProvider.php @@ -0,0 +1,26 @@ + + * + * 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\Fixtures\TestBundle\Parameter; + +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\State\ParameterProviderInterface; + +final class CustomGroupParameterProvider implements ParameterProviderInterface +{ + public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?HttpOperation + { + return $context['operation']->withNormalizationContext(['groups' => 'custom']); + } +} diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index d10205c1b6d..80a425f5ba7 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -440,3 +440,8 @@ services: api_platform.http_cache.tag_collector: class: ApiPlatform\Tests\Fixtures\TestBundle\HttpCache\TagCollectorDefault public: true + + ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider: + tags: + - name: 'api_platform.parameter_provider' + key: 'ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider' diff --git a/tests/Parameter/ParameterTests.php b/tests/Parameter/ParameterTests.php new file mode 100644 index 00000000000..dc8213cd5e4 --- /dev/null +++ b/tests/Parameter/ParameterTests.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\Parameter; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; + +final class ParameterTests extends ApiTestCase +{ + public function testWithGroupFilter(): void + { + $response = self::createClient()->request('GET', 'with_parameters?groups[]=b'); + $this->assertArraySubset(['b' => 'bar'], $response->toArray()); + $response = self::createClient()->request('GET', 'with_parameters?groups[]=b&groups[]=a'); + $this->assertArraySubset(['a' => 'foo', 'b' => 'bar'], $response->toArray()); + } + + public function testWithGroupProvider(): void + { + $response = self::createClient()->request('GET', 'with_parameters?group[]=b&group[]=a'); + $this->assertArraySubset(['a' => 'foo', 'b' => 'bar'], $response->toArray()); + } + + public function testWithServiceFilter(): void + { + $response = self::createClient()->request('GET', 'with_parameters?properties[]=a'); + $this->assertArraySubset(['a' => 'foo'], $response->toArray()); + } + + public function testWithServiceProvider(): void + { + $response = self::createClient()->request('GET', 'with_parameters?service=blabla'); + $this->assertArrayNotHasKey('a', $response->toArray()); + } + + public function testWithHeader(): void + { + $response = self::createClient()->request('GET', 'with_parameters?service=blabla', ['headers' => ['auth' => 'foo']]); + $this->assertResponseStatusCodeSame(401); + } +}