From ed7a9e6e8641ecaff3362bff0091112229b0f608 Mon Sep 17 00:00:00 2001 From: soyuka Date: Mon, 18 Mar 2024 16:00:30 +0100 Subject: [PATCH] xml, yaml, priority parameters --- .../Orm/Extension/ParameterExtension.php | 76 ++++++++++++++ .../CollectionFiltersNormalizer.php | 3 +- src/Metadata/ApiResource.php | 2 +- src/Metadata/Delete.php | 2 + .../Extractor/XmlResourceExtractor.php | 21 ++++ .../Extractor/YamlResourceExtractor.php | 23 ++++- src/Metadata/Get.php | 2 +- src/Metadata/GetCollection.php | 2 +- src/Metadata/GraphQl/Operation.php | 3 +- src/Metadata/GraphQl/Query.php | 3 +- src/Metadata/GraphQl/QueryCollection.php | 3 +- src/Metadata/GraphQl/Subscription.php | 3 +- src/Metadata/HttpOperation.php | 2 +- src/Metadata/Metadata.php | 8 +- src/Metadata/Operation.php | 2 +- src/Metadata/Parameter.php | 14 +++ src/Metadata/Parameters.php | 98 +++++++++++++++++++ src/Metadata/Patch.php | 2 +- src/Metadata/Post.php | 2 +- src/Metadata/Put.php | 2 +- ...meterResourceMetadataCollectionFactory.php | 87 +++++++++------- src/OpenApi/Factory/OpenApiFactory.php | 4 +- src/State/Provider/ParameterProvider.php | 4 +- .../Bundle/Resources/config/doctrine_orm.xml | 7 ++ .../TestBundle/ApiResource/WithParameter.php | 16 ++- .../Entity/SearchFilterParameter.php | 42 ++++++++ tests/Fixtures/app/config/config_common.yml | 5 + tests/Parameter/ParameterTests.php | 44 +++++++++ 28 files changed, 423 insertions(+), 59 deletions(-) create mode 100644 src/Doctrine/Orm/Extension/ParameterExtension.php create mode 100644 src/Metadata/Parameters.php create mode 100644 tests/Fixtures/TestBundle/Entity/SearchFilterParameter.php diff --git a/src/Doctrine/Orm/Extension/ParameterExtension.php b/src/Doctrine/Orm/Extension/ParameterExtension.php new file mode 100644 index 00000000000..ba39b4a7765 --- /dev/null +++ b/src/Doctrine/Orm/Extension/ParameterExtension.php @@ -0,0 +1,76 @@ + + * + * 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\Doctrine\Orm\Extension; + +use ApiPlatform\Doctrine\Orm\Filter\FilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\HeaderParameterInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\QueryBuilder; +use Psr\Container\ContainerInterface; + +/** + * @author Antoine Bluchet + */ +final class ParameterExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface +{ + public function __construct(private readonly ContainerInterface $filterLocator) + { + } + + private function applyFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, ?string $resourceClass = null, ?Operation $operation = null, array $context = []) { + if (!($request = $context['request'] ?? null)) { + return; + } + + if (null === $resourceClass) { + throw new InvalidArgumentException('The "$resourceClass" parameter must not be null'); + } + + foreach ($operation->getParameters() ?? [] as $parameter) { + $key = $parameter->getKey(); + if (null === ($filterId = $parameter->getFilter())) { + continue; + } + + $parameters = $parameter instanceof HeaderParameterInterface ? $request->attributes->get('_api_header_parameters') : $request->attributes->get('_api_query_parameters'); + if (!isset($parameters[$key])) { + continue; + } + + $filter = $this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null; + if ($filter instanceof FilterInterface) { + $filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, ['filters' => [$parameter->getProperty() ?? $key => $parameters[$key]]] + $context); + } + } + } + + /** + * {@inheritdoc} + */ + public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, ?string $resourceClass = null, ?Operation $operation = null, array $context = []): void + { + $this->applyFilter($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context); + } + + /** + * {@inheritdoc} + */ + public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, ?Operation $operation = null, array $context = []): void + { + $this->applyFilter($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context); + } +} diff --git a/src/Hydra/Serializer/CollectionFiltersNormalizer.php b/src/Hydra/Serializer/CollectionFiltersNormalizer.php index f404e9b731e..54f6cbcd93c 100644 --- a/src/Hydra/Serializer/CollectionFiltersNormalizer.php +++ b/src/Hydra/Serializer/CollectionFiltersNormalizer.php @@ -19,6 +19,7 @@ use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata\FilterInterface; use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\Parameters; use ApiPlatform\Metadata\QueryParameterInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; @@ -150,7 +151,7 @@ public function setNormalizer(NormalizerInterface $normalizer): void * @param LegacyFilterInterface[]|FilterInterface[] $filters * @param array $parameters */ - private function getSearch(string $resourceClass, array $parts, array $filters, ?array $parameters): array + private function getSearch(string $resourceClass, array $parts, array $filters, null|array|Parameters $parameters): array { $variables = []; $mapping = []; diff --git a/src/Metadata/ApiResource.php b/src/Metadata/ApiResource.php index b07e71a1410..2907cc5d47f 100644 --- a/src/Metadata/ApiResource.php +++ b/src/Metadata/ApiResource.php @@ -960,7 +960,7 @@ public function __construct( $provider = null, $processor = null, protected ?OptionsInterface $stateOptions = null, - protected ?array $parameters = null, + protected null|array|Parameters $parameters = null, protected array $extraProperties = [], ) { parent::__construct( diff --git a/src/Metadata/Delete.php b/src/Metadata/Delete.php index 1b816cb6558..eeeaa187008 100644 --- a/src/Metadata/Delete.php +++ b/src/Metadata/Delete.php @@ -94,6 +94,7 @@ public function __construct( $provider = null, $processor = null, ?OptionsInterface $stateOptions = null, + null|array|Parameters $parameters = null, array $extraProperties = [], ) { parent::__construct( @@ -170,6 +171,7 @@ class: $class, processor: $processor, extraProperties: $extraProperties, collectDenormalizationErrors: $collectDenormalizationErrors, + parameters: $parameters, stateOptions: $stateOptions, ); } diff --git a/src/Metadata/Extractor/XmlResourceExtractor.php b/src/Metadata/Extractor/XmlResourceExtractor.php index cb29ccdcaa1..48c71dde349 100644 --- a/src/Metadata/Extractor/XmlResourceExtractor.php +++ b/src/Metadata/Extractor/XmlResourceExtractor.php @@ -515,6 +515,27 @@ private function buildParameters(\SimpleXMLElement $resource): ?array key: $key, required: $this->phpize($parameter, 'required', 'bool'), schema: isset($parameter->schema->values) ? $this->buildValues($parameter->schema->values) : null, + openApi: isset($parameter->openapi) ? new OpenApiParameter( + name: $this->phpize($parameter->openapi, 'name', 'string'), + in: $this->phpize($parameter->openapi, 'in', 'string'), + description: $this->phpize($parameter->openapi, 'description', 'string'), + required: $this->phpize($parameter->openapi, 'required', 'bool'), + deprecated: $this->phpize($parameter->openapi, 'deprecated', 'bool'), + allowEmptyValue: $this->phpize($parameter->openapi, 'allowEmptyValue', 'bool'), + schema: isset($parameter->openapi->schema->values) ? $this->buildValues($parameter->openapi->schema->values) : null, + style: $this->phpize($parameter->openapi, 'style', 'string'), + explode: $this->phpize($parameter->openapi, 'explode', 'bool'), + allowReserved: $this->phpize($parameter->openapi, 'allowReserved', 'bool'), + example: $this->phpize($parameter->openapi, 'example', 'string'), + examples: isset($parameter->openapi->examples->values) ? new \ArrayObject($this->buildValues($parameter->openapi->examples->values)) : null, + content: isset($parameter->openapi->content->values) ? new \ArrayObject($this->buildValues($parameter->openapi->content->values)) : null, + ) : null, + provider: $this->phpize($parameter, 'provider', 'string'), + filter: $this->phpize($parameter, 'filter', 'string'), + property: $this->phpize($parameter, 'property', 'string'), + description: $this->phpize($parameter, 'description', 'string'), + priority: $this->phpize($parameter, 'priority', 'integer'), + extraProperties: $this->buildExtraProperties($parameter, 'extraProperties'), ); } diff --git a/src/Metadata/Extractor/YamlResourceExtractor.php b/src/Metadata/Extractor/YamlResourceExtractor.php index 3efc8beadd4..089b0f643e2 100644 --- a/src/Metadata/Extractor/YamlResourceExtractor.php +++ b/src/Metadata/Extractor/YamlResourceExtractor.php @@ -469,7 +469,28 @@ private function buildParameters(array $resource): ?array $parameters[$key] = new $cl( key: $key, required: $this->phpize($parameter, 'required', 'bool'), - schema: $parameter['schema'] + schema: $parameter['schema'], + openapi: ($parameter['openapi'] ?? null) ? new Parameter( + name: $parameter['openapi']['name'], + in: $parameter['in'] ?? 'query', + description: $parameter['openapi']['description'] ?? '', + required: $parameter['openapi']['required'] ?? $parameter['required'] ?? false, + deprecated: $parameter['openapi']['deprecated'] ?? false, + allowEmptyValue: $parameter['openapi']['allowEmptyValue'] ?? false, + schema: $parameter['openapi']['schema'] ?? $parameter['schema'] ?? [], + style: $parameter['openapi']['style'] ?? null, + explode: $parameter['openapi']['explode'] ?? false, + allowReserved: $parameter['openapi']['allowReserved '] ?? false, + example: $parameter['openapi']['example'] ?? null, + examples: isset($parameter['openapi']['examples']) ? new \ArrayObject($parameter['openapi']['examples']) : null, + content: isset($parameter['openapi']['content']) ? new \ArrayObject($parameter['openapi']['content']) : null + ) : null, + provider: $this->phpize($parameter, 'provider', 'string'), + filter: $this->phpize($parameter, 'filter', 'string'), + property: $this->phpize($parameter, 'property', 'string'), + description: $this->phpize($parameter, 'description', 'string'), + priority: $this->phpize($parameter, 'priority', 'integer'), + extraProperties: $this->buildArrayValue($resource, 'extraProperties'), ); } diff --git a/src/Metadata/Get.php b/src/Metadata/Get.php index 109d816311d..0a60f21f34b 100644 --- a/src/Metadata/Get.php +++ b/src/Metadata/Get.php @@ -94,7 +94,7 @@ public function __construct( $provider = null, $processor = null, ?OptionsInterface $stateOptions = null, - ?array $parameters = null, + null|array|Parameters $parameters = null, array $extraProperties = [], ) { parent::__construct( diff --git a/src/Metadata/GetCollection.php b/src/Metadata/GetCollection.php index 838a2f7e1d3..74ba34c6cf0 100644 --- a/src/Metadata/GetCollection.php +++ b/src/Metadata/GetCollection.php @@ -94,7 +94,7 @@ public function __construct( $provider = null, $processor = null, ?OptionsInterface $stateOptions = null, - ?array $parameters = null, + null|array|Parameters $parameters = null, array $extraProperties = [], private ?string $itemUriTemplate = null, ) { diff --git a/src/Metadata/GraphQl/Operation.php b/src/Metadata/GraphQl/Operation.php index d4e3cfb026e..3b502a5d373 100644 --- a/src/Metadata/GraphQl/Operation.php +++ b/src/Metadata/GraphQl/Operation.php @@ -15,6 +15,7 @@ use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Operation as AbstractOperation; +use ApiPlatform\Metadata\Parameters; use ApiPlatform\State\OptionsInterface; class Operation extends AbstractOperation @@ -84,7 +85,7 @@ public function __construct( $provider = null, $processor = null, ?OptionsInterface $stateOptions = null, - ?array $parameters = null, + null|array|Parameters $parameters = null, array $extraProperties = [] ) { parent::__construct( diff --git a/src/Metadata/GraphQl/Query.php b/src/Metadata/GraphQl/Query.php index 59bb5c924ec..288968d7a6e 100644 --- a/src/Metadata/GraphQl/Query.php +++ b/src/Metadata/GraphQl/Query.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Metadata\GraphQl; +use ApiPlatform\Metadata\Parameters; use ApiPlatform\State\OptionsInterface; #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] @@ -68,7 +69,7 @@ public function __construct( $provider = null, $processor = null, ?OptionsInterface $stateOptions = null, - ?array $parameters = null, + null|array|Parameters $parameters = null, array $extraProperties = [], protected ?bool $nested = null, diff --git a/src/Metadata/GraphQl/QueryCollection.php b/src/Metadata/GraphQl/QueryCollection.php index 0d5bedb5ced..f1d9cf6606d 100644 --- a/src/Metadata/GraphQl/QueryCollection.php +++ b/src/Metadata/GraphQl/QueryCollection.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Metadata\GraphQl; use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Parameters; use ApiPlatform\State\OptionsInterface; #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] @@ -69,7 +70,7 @@ public function __construct( $provider = null, $processor = null, protected ?OptionsInterface $stateOptions = null, - ?array $parameters = null, + null|array|Parameters $parameters = null, array $extraProperties = [], ?bool $nested = null, diff --git a/src/Metadata/GraphQl/Subscription.php b/src/Metadata/GraphQl/Subscription.php index 3e096304635..eade951a501 100644 --- a/src/Metadata/GraphQl/Subscription.php +++ b/src/Metadata/GraphQl/Subscription.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Metadata\GraphQl; +use ApiPlatform\Metadata\Parameters; use ApiPlatform\State\OptionsInterface; #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] @@ -68,7 +69,7 @@ public function __construct( $provider = null, $processor = null, ?OptionsInterface $stateOptions = null, - ?array $parameters = null, + null|array|Parameters $parameters = null, array $extraProperties = [], ) { parent::__construct( diff --git a/src/Metadata/HttpOperation.php b/src/Metadata/HttpOperation.php index 62ce886a250..ce12e9c58b4 100644 --- a/src/Metadata/HttpOperation.php +++ b/src/Metadata/HttpOperation.php @@ -200,7 +200,7 @@ public function __construct( $provider = null, $processor = null, ?OptionsInterface $stateOptions = null, - ?array $parameters = null, + null|array|Parameters $parameters = null, array $extraProperties = [], ) { parent::__construct( diff --git a/src/Metadata/Metadata.php b/src/Metadata/Metadata.php index de382e52b01..2472480353c 100644 --- a/src/Metadata/Metadata.php +++ b/src/Metadata/Metadata.php @@ -30,7 +30,7 @@ abstract class Metadata * @param mixed|null $output * @param mixed|null $provider * @param mixed|null $processor - * @param array $parameters + * @param Parameters|array $parameters */ public function __construct( protected ?string $shortName = null, @@ -73,7 +73,7 @@ public function __construct( /** * @experimental */ - protected ?array $parameters = [], + protected null|array|Parameters $parameters = [], protected array $extraProperties = [] ) { } @@ -574,12 +574,12 @@ public function withStateOptions(?OptionsInterface $stateOptions): static /** * @return array */ - public function getParameters(): ?array + public function getParameters(): null|array|Parameters { return $this->parameters; } - public function withParameters(array $parameters): static + public function withParameters(array|Parameters $parameters): static { $self = clone $this; $self->parameters = $parameters; diff --git a/src/Metadata/Operation.php b/src/Metadata/Operation.php index e5dd58f5985..f4582c8c724 100644 --- a/src/Metadata/Operation.php +++ b/src/Metadata/Operation.php @@ -806,7 +806,7 @@ public function __construct( protected $provider = null, protected $processor = null, protected ?OptionsInterface $stateOptions = null, - protected ?array $parameters = [], + protected null|array|Parameters $parameters = [], protected array $extraProperties = [], ) { parent::__construct( diff --git a/src/Metadata/Parameter.php b/src/Metadata/Parameter.php index 011bb4b2131..832f840a83f 100644 --- a/src/Metadata/Parameter.php +++ b/src/Metadata/Parameter.php @@ -36,6 +36,7 @@ public function __construct( protected ?string $property = null, protected ?string $description = null, protected ?bool $required = null, + protected ?int $priority = null, protected array $extraProperties = [], ) { } @@ -83,6 +84,11 @@ public function getRequired(): ?bool return $this->required; } + public function getPriority(): ?int + { + return $this->priority; + } + /** * @return array */ @@ -99,6 +105,14 @@ public function withKey(string $key): static return $self; } + public function withPriority(int $priority): static + { + $self = clone $this; + $self->priority = $priority; + + return $self; + } + /** * @param array{type?: string} $schema */ diff --git a/src/Metadata/Parameters.php b/src/Metadata/Parameters.php new file mode 100644 index 00000000000..f0b6afdbf8d --- /dev/null +++ b/src/Metadata/Parameters.php @@ -0,0 +1,98 @@ + + * + * 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; + +/** + * An parameter dictionnary. + */ +final class Parameters implements \IteratorAggregate, \Countable +{ + private array $parameters = []; + + /** + * @param array $parameters + */ + public function __construct(array $parameters = []) + { + foreach ($parameters as $parameterName => $parameter) { + if ($parameter->getKey()) { + $parameterName = $parameter->getKey(); + } + + $this->parameters[] = [$parameterName, $parameter]; + } + + $this->sort(); + } + + public function getIterator(): \Traversable + { + return (function (): \Generator { + foreach ($this->parameters as [$parameterName, $parameter]) { + yield $parameterName => $parameter; + } + })(); + } + + public function add(string $key, Parameter $value): self + { + foreach ($this->parameters as $i => [$parameterName, $parameter]) { + if ($parameterName === $key) { + $this->parameters[$i] = [$key, $value]; + + return $this; + } + } + + $this->parameters[] = [$key, $value]; + + return $this; + } + + public function remove(string $key): self + { + foreach ($this->parameters as $i => [$parameterName, $parameter]) { + if ($parameterName === $key) { + unset($this->parameters[$i]); + + return $this; + } + } + + throw new \RuntimeException(sprintf('Could not remove parameter "%s".', $key)); + } + + public function has(string $key): bool + { + foreach ($this->parameters as $i => [$parameterName, $parameter]) { + if ($parameterName === $key) { + return true; + } + } + + return false; + } + + public function count(): int + { + return \count($this->parameters); + } + + public function sort(): self + { + usort($this->parameters, fn ($a, $b): int|float => $b[1]->getPriority() - $a[1]->getPriority()); + + return $this; + } +} diff --git a/src/Metadata/Patch.php b/src/Metadata/Patch.php index a1d8881d86d..043c07db620 100644 --- a/src/Metadata/Patch.php +++ b/src/Metadata/Patch.php @@ -94,7 +94,7 @@ public function __construct( $provider = null, $processor = null, ?OptionsInterface $stateOptions = null, - ?array $parameters = null, + null|array|Parameters $parameters = null, array $extraProperties = [], ) { parent::__construct( diff --git a/src/Metadata/Post.php b/src/Metadata/Post.php index b54d2686bf7..3d609f4adc7 100644 --- a/src/Metadata/Post.php +++ b/src/Metadata/Post.php @@ -94,7 +94,7 @@ public function __construct( $provider = null, $processor = null, ?OptionsInterface $stateOptions = null, - ?array $parameters = null, + null|array|Parameters $parameters = null, array $extraProperties = [], private ?string $itemUriTemplate = null ) { diff --git a/src/Metadata/Put.php b/src/Metadata/Put.php index 5e45a9076c3..b487493b364 100644 --- a/src/Metadata/Put.php +++ b/src/Metadata/Put.php @@ -94,7 +94,7 @@ public function __construct( $provider = null, $processor = null, ?OptionsInterface $stateOptions = null, - ?array $parameters = null, + null|array|Parameters $parameters = null, array $extraProperties = [], private ?bool $allowCreate = null, ) { diff --git a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php index 6559f0d69dd..af5f952e116 100644 --- a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php @@ -15,9 +15,11 @@ use ApiPlatform\Metadata\FilterInterface; use ApiPlatform\Metadata\HeaderParameterInterface; +use ApiPlatform\Metadata\Parameters; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\OpenApi; use ApiPlatform\Serializer\Filter\FilterInterface as SerializerFilterInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\WithParameter; use Psr\Container\ContainerInterface; /** @@ -36,6 +38,7 @@ public function create(string $resourceClass): ResourceMetadataCollection foreach ($resourceMetadataCollection as $i => $resource) { $operations = $resource->getOperations(); + $internalPriority = -1; foreach ($operations as $operationName => $operation) { $parameters = []; foreach ($operation->getParameters() ?? [] as $key => $parameter) { @@ -48,54 +51,45 @@ public function create(string $resourceClass): ResourceMetadataCollection $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()) { + $description = $filter instanceof FilterInterface ? $filter->getDescription($resourceClass) : []; + if (($schema = $description['schema'] ?? null) && 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 (null === $parameter->getOpenApi() && $openApi = $description['openapi'] ?? null) { + if ($openApi instanceof OpenApi\Model\Parameter) { + $parameter = $parameter->withOpenApi($openApi); + } + + if (\is_array($openApi)) { + $parameter->withOpenApi(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 + )); + } } - 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; + $priority = $parameter->getPriority() ?? $internalPriority--; + $parameters[$key] = $parameter->withPriority($priority); } - $operations->add($operationName, $operation->withParameters($parameters)); + $operations->add($operationName, $operation->withParameters(new Parameters($parameters))); } $resourceMetadataCollection[$i] = $resource->withOperations($operations->sort()); @@ -103,4 +97,25 @@ public function create(string $resourceClass): ResourceMetadataCollection return $resourceMetadataCollection; } + + /** + * @return Iterable + */ + private function getIterator(null|array|\SplPriorityQueue $parameters): Iterable { + if (!$parameters) { + return []; + } + + if (is_array($parameters)) { + foreach ($parameters as $key => $parameter) { + yield $key => $parameter; + } + + return $parameters; + } + + foreach ($parameters as $priority => $parameter) { + yield $parameter->getKey() => $parameter->withPriority($priority); + } + } } diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index b059d9c183d..4534ec429f7 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -308,9 +308,9 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection } $openapiParameters = $openapiOperation->getParameters(); - foreach ($operation->getParameters() ?? [] as $key => $p) { + foreach ($operation->getParameters() ?? [] as $p) { $in = $p instanceof HeaderParameterInterface ? 'header' : 'query'; - $parameter = new Parameter($key, $in, $p->getDescription() ?? "$resourceShortName $key", $p->getRequired() ?? false, false, false, $p->getSchema() ?? ['type' => 'string']); + $parameter = new Parameter($p->getKey(), $in, $p->getDescription() ?? "$resourceShortName $key", $p->getRequired() ?? false, false, false, $p->getSchema() ?? ['type' => 'string']); if ($linkParameter = $p->getOpenApi()) { $parameter = $this->mergeParameter($parameter, $linkParameter); diff --git a/src/State/Provider/ParameterProvider.php b/src/State/Provider/ParameterProvider.php index a4681d77539..1da3f623036 100644 --- a/src/State/Provider/ParameterProvider.php +++ b/src/State/Provider/ParameterProvider.php @@ -47,8 +47,8 @@ public function provide(Operation $operation, array $uriVariables = [], array $c } $context = ['operation' => $operation] + $context; - - foreach ($operation->getParameters() ?? [] as $key => $parameter) { + foreach ($operation->getParameters() ?? [] as $parameter) { + $key = $parameter->getKey(); if (null === ($provider = $parameter->getProvider())) { continue; } diff --git a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml index ad0d77a5121..0a6228a4e77 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -120,6 +120,13 @@ + + + + + + + diff --git a/tests/Fixtures/TestBundle/ApiResource/WithParameter.php b/tests/Fixtures/TestBundle/ApiResource/WithParameter.php index 93534d8ba07..638095fbcc4 100644 --- a/tests/Fixtures/TestBundle/ApiResource/WithParameter.php +++ b/tests/Fixtures/TestBundle/ApiResource/WithParameter.php @@ -25,7 +25,7 @@ use Symfony\Component\Serializer\Attribute\Groups; #[Get( - uriTemplate: 'with_parameters/{id}', + uriTemplate: 'with_parameters/{id}{._format}', uriVariables: [ 'id' => new Link(schema: ['type' => 'uuid'], property: 'id'), ], @@ -35,6 +35,8 @@ 'properties' => new QueryParameter(filter: 'my_dummy.property'), 'service' => new QueryParameter(provider: CustomGroupParameterProvider::class), 'auth' => new HeaderParameter(provider: [self::class, 'restrictAccess']), + 'priority' => new QueryParameter(provider: [self::class, 'assertSecond'], priority: 10), + 'priorityb' => new QueryParameter(provider: [self::class, 'assertFirst'], priority: 20), ], provider: [self::class, 'provide'] )] @@ -47,6 +49,7 @@ )] class WithParameter { + private static int $counter = 1; public int $id = 1; #[Groups(['a'])] @@ -64,6 +67,17 @@ public static function provide() return new self(); } + public static function assertFirst() + { + assert(static::$counter === 1); + static::$counter++; + } + + public static function assertSecond() + { + assert(static::$counter === 2); + } + public static function provideGroup(Parameter $parameter, array $parameters = [], array $context = []) { $operation = $context['operation']; diff --git a/tests/Fixtures/TestBundle/Entity/SearchFilterParameter.php b/tests/Fixtures/TestBundle/Entity/SearchFilterParameter.php new file mode 100644 index 00000000000..7a29adcd9aa --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/SearchFilterParameter.php @@ -0,0 +1,42 @@ + new QueryParameter(filter: 'app_search_filter_via_parameter', property: 'foo'), + ] +)] +#[ORM\Entity] +class SearchFilterParameter +{ + /** + * @var int The id + */ + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private ?int $id = null; + #[ORM\Column(type: 'string')] + private string $foo = ''; + + public function getId(): ?int + { + return $this->id; + } + + public function getFoo(): string + { + return $this->foo; + } + + public function setFoo(string $foo): void + { + $this->foo = $foo; + } +} diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 7bd2b6b82a5..332099bc075 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -450,3 +450,8 @@ services: tags: - name: 'api_platform.parameter_provider' key: 'ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider' + + app_search_filter_via_parameter: + parent: 'api_platform.doctrine.orm.search_filter' + arguments: [ { foo: 'exact' } ] + tags: [ { name: 'api_platform.filter', id: 'app_search_filter_via_parameter' } ] diff --git a/tests/Parameter/ParameterTests.php b/tests/Parameter/ParameterTests.php index e6b0e4deb63..f472bed1749 100644 --- a/tests/Parameter/ParameterTests.php +++ b/tests/Parameter/ParameterTests.php @@ -14,6 +14,8 @@ namespace ApiPlatform\Tests\Parameter; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SearchFilterParameter; +use Doctrine\ORM\Tools\SchemaTool; final class ParameterTests extends ApiTestCase { @@ -59,4 +61,46 @@ public function testHydraTemplate(): void ], ]], $response->toArray()); } + + public function testDoctrineEntitySearchFilter(): void + { + $this->recreateSchema(); + $registry = $this->getContainer()->get('doctrine'); + $entityManager = $registry->getManagerForClass(SearchFilterParameter::class); + + foreach (['foo', 'foo', 'foo', 'bar', 'bar'] as $t) { + $s = new SearchFilterParameter(); + $s->setFoo($t); + $entityManager->persist($s); + } + $entityManager->flush(); + + $response = self::createClient()->request('GET', 'search_filter_parameter?search=bar'); + $a = $response->toArray(); + $this->assertCount(2, $a['hydra:member']); + $this->assertEquals('bar', $a['hydra:member'][0]['foo']); + $this->assertEquals('bar', $a['hydra:member'][1]['foo']); + + $this->assertArraySubset(['hydra:search' => [ + 'hydra:template' => '/search_filter_parameter{?search}', + 'hydra:mapping' => [ + ['@type' => 'IriTemplateMapping', 'variable' => 'search', 'property' => 'foo'], + ], + ]], $a); + } + + private function recreateSchema(array $options = []): void + { + self::bootKernel($options); + + /** @var EntityManagerInterface $manager */ + $manager = static::getContainer()->get('doctrine')->getManager(); + /** @var ClassMetadata[] $classes */ + $classes = $manager->getMetadataFactory()->getAllMetadata(); + $schemaTool = new SchemaTool($manager); + + @$schemaTool->dropSchema($classes); + @$schemaTool->createSchema($classes); + } + }