Skip to content

Commit fc8fa00

Browse files
authored
fix(hydra): iri template when using query parameter (#6742)
1 parent 77d3ff3 commit fc8fa00

File tree

7 files changed

+214
-113
lines changed

7 files changed

+214
-113
lines changed

src/Hydra/Serializer/CollectionFiltersNormalizer.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ private function getSearch(string $resourceClass, array $parts, array $filters,
171171
continue;
172172
}
173173

174-
if (!($property = $parameter->getProperty()) && ($filterId = $parameter->getFilter()) && ($filter = $this->getFilter($filterId))) {
174+
if (($filterId = $parameter->getFilter()) && ($filter = $this->getFilter($filterId))) {
175175
foreach ($filter->getDescription($resourceClass) as $variable => $description) {
176176
// This is a practice induced by PHP and is not necessary when implementing URI template
177177
if (str_ends_with((string) $variable, '[]')) {
@@ -192,7 +192,7 @@ private function getSearch(string $resourceClass, array $parts, array $filters,
192192
continue;
193193
}
194194

195-
if (!$property) {
195+
if (!($property = $parameter->getProperty())) {
196196
continue;
197197
}
198198

src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use ApiPlatform\Doctrine\Odm\State\Options as DoctrineOdmOptions;
1717
use ApiPlatform\Doctrine\Orm\State\Options as DoctrineOrmOptions;
1818
use ApiPlatform\Metadata\FilterInterface;
19-
use ApiPlatform\Metadata\HeaderParameterInterface;
2019
use ApiPlatform\Metadata\HttpOperation;
2120
use ApiPlatform\Metadata\Operation;
2221
use ApiPlatform\Metadata\Parameter;
@@ -126,38 +125,10 @@ private function setDefaults(string $key, Parameter $parameter, string $resource
126125
$parameter = $parameter->withSchema($schema);
127126
}
128127

129-
if (null === $parameter->getProperty() && ($property = $description[$key]['property'] ?? null)) {
130-
$parameter = $parameter->withProperty($property);
131-
}
132-
133128
if (null === $parameter->getRequired() && ($required = $description[$key]['required'] ?? null)) {
134129
$parameter = $parameter->withRequired($required);
135130
}
136131

137-
if (null === $parameter->getOpenApi() && $openApi = $description[$key]['openapi'] ?? null) {
138-
if ($openApi instanceof OpenApiParameter) {
139-
$parameter = $parameter->withOpenApi($openApi);
140-
} elseif (\is_array($openApi)) {
141-
$schema = $schema ?? $openApi['schema'] ?? [];
142-
$parameter = $parameter->withOpenApi(new OpenApiParameter(
143-
$key,
144-
$parameter instanceof HeaderParameterInterface ? 'header' : 'query',
145-
$description[$key]['description'] ?? '',
146-
$description[$key]['required'] ?? $openApi['required'] ?? false,
147-
$openApi['deprecated'] ?? false,
148-
$openApi['allowEmptyValue'] ?? true,
149-
$schema,
150-
$openApi['style'] ?? null,
151-
$openApi['explode'] ?? ('array' === ($schema['type'] ?? null)),
152-
$openApi['allowReserved'] ?? false,
153-
$openApi['example'] ?? null,
154-
isset(
155-
$openApi['examples']
156-
) ? new \ArrayObject($openApi['examples']) : null
157-
));
158-
}
159-
}
160-
161132
$schema = $parameter->getSchema() ?? (($openApi = $parameter->getOpenApi()) ? $openApi->getSchema() : null);
162133

163134
// Only add validation if the Symfony Validator is installed

src/OpenApi/Factory/OpenApiFactory.php

Lines changed: 88 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -331,12 +331,26 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
331331
}
332332
}
333333

334+
$entityClass = $this->getFilterClass($operation);
334335
$openapiParameters = $openapiOperation->getParameters();
335336
foreach ($operation->getParameters() ?? [] as $key => $p) {
336337
if (false === $p->getOpenApi()) {
337338
continue;
338339
}
339340

341+
if (($f = $p->getFilter()) && \is_string($f) && $this->filterLocator->has($f)) {
342+
$filter = $this->filterLocator->get($f);
343+
foreach ($filter->getDescription($entityClass) as $name => $description) {
344+
if ($prop = $p->getProperty()) {
345+
$name = str_replace($prop, $key, $name);
346+
}
347+
348+
$openapiParameters[] = $this->getFilterParameter($name, $description, $operation->getShortName(), $f);
349+
}
350+
351+
continue;
352+
}
353+
340354
$in = $p instanceof HeaderParameterInterface ? 'header' : 'query';
341355
$parameter = new Parameter($key, $in, $p->getDescription() ?? "$resourceShortName $key", $p->getRequired() ?? false, false, false, $p->getSchema() ?? ['type' => 'string']);
342356

@@ -654,92 +668,102 @@ private function getLinks(ResourceMetadataCollection $resourceMetadataCollection
654668
private function getFiltersParameters(CollectionOperationInterface|HttpOperation $operation): array
655669
{
656670
$parameters = [];
657-
658671
$resourceFilters = $operation->getFilters();
672+
$entityClass = $this->getFilterClass($operation);
673+
659674
foreach ($resourceFilters ?? [] as $filterId) {
660675
if (!$this->filterLocator->has($filterId)) {
661676
continue;
662677
}
663678

664679
$filter = $this->filterLocator->get($filterId);
665-
$entityClass = $operation->getClass();
666-
if ($options = $operation->getStateOptions()) {
667-
if ($options instanceof DoctrineOptions && $options->getEntityClass()) {
668-
$entityClass = $options->getEntityClass();
669-
}
670-
671-
if ($options instanceof DoctrineODMOptions && $options->getDocumentClass()) {
672-
$entityClass = $options->getDocumentClass();
673-
}
680+
foreach ($filter->getDescription($entityClass) as $name => $description) {
681+
$parameters[] = $this->getFilterParameter($name, $description, $operation->getShortName(), $filterId);
674682
}
683+
}
675684

676-
foreach ($filter->getDescription($entityClass) as $name => $data) {
677-
if (isset($data['swagger'])) {
678-
trigger_deprecation('api-platform/core', '4.0', \sprintf('Using the "swagger" field of the %s::getDescription() (%s) is deprecated.', $filter::class, $operation->getShortName()));
679-
}
685+
return $parameters;
686+
}
680687

681-
if (!isset($data['openapi']) || $data['openapi'] instanceof Parameter) {
682-
$schema = $data['schema'] ?? [];
688+
private function getFilterClass(HttpOperation $operation): ?string
689+
{
690+
$entityClass = $operation->getClass();
691+
if ($options = $operation->getStateOptions()) {
692+
if ($options instanceof DoctrineOptions && $options->getEntityClass()) {
693+
return $options->getEntityClass();
694+
}
683695

684-
if (isset($data['type']) && \in_array($data['type'] ?? null, Type::$builtinTypes, true) && !isset($schema['type'])) {
685-
$schema += $this->jsonSchemaTypeFactory ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false)) : $this->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false));
686-
}
696+
if ($options instanceof DoctrineODMOptions && $options->getDocumentClass()) {
697+
return $options->getDocumentClass();
698+
}
699+
}
687700

688-
if (!isset($schema['type'])) {
689-
$schema['type'] = 'string';
690-
}
701+
return $entityClass;
702+
}
691703

692-
$style = 'array' === ($schema['type'] ?? null) && \in_array(
693-
$data['type'],
694-
[Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT],
695-
true
696-
) ? 'deepObject' : 'form';
704+
private function getFilterParameter(string $name, array $description, string $shortName, string $filter): Parameter
705+
{
706+
if (isset($description['swagger'])) {
707+
trigger_deprecation('api-platform/core', '4.0', \sprintf('Using the "swagger" field of the %s::getDescription() (%s) is deprecated.', $filter, $shortName));
708+
}
697709

698-
$parameter = isset($data['openapi']) && $data['openapi'] instanceof Parameter ? $data['openapi'] : new Parameter(in: 'query', name: $name, style: $style, explode: $data['is_collection'] ?? false);
710+
if (!isset($description['openapi']) || $description['openapi'] instanceof Parameter) {
711+
$schema = $description['schema'] ?? [];
699712

700-
if ('' === $parameter->getDescription() && ($description = $data['description'] ?? '')) {
701-
$parameter = $parameter->withDescription($description);
702-
}
713+
if (isset($description['type']) && \in_array($description['type'], Type::$builtinTypes, true) && !isset($schema['type'])) {
714+
$schema += $this->jsonSchemaTypeFactory ? $this->jsonSchemaTypeFactory->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false)) : $this->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false));
715+
}
703716

704-
if (false === $parameter->getRequired() && false !== ($required = $data['required'] ?? false)) {
705-
$parameter = $parameter->withRequired($required);
706-
}
717+
if (!isset($schema['type'])) {
718+
$schema['type'] = 'string';
719+
}
707720

708-
$parameters[] = $parameter->withSchema($schema);
709-
continue;
710-
}
721+
$style = 'array' === ($schema['type'] ?? null) && \in_array(
722+
$description['type'],
723+
[Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT],
724+
true
725+
) ? 'deepObject' : 'form';
711726

712-
trigger_deprecation('api-platform/core', '4.0', \sprintf('Not using "%s" on the "openapi" field of the %s::getDescription() (%s) is deprecated.', Parameter::class, $filter::class, $operation->getShortName()));
713-
if ($this->jsonSchemaTypeFactory) {
714-
$schema = $data['schema'] ?? (\in_array($data['type'], Type::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false), 'openapi') : ['type' => 'string']);
715-
} else {
716-
$schema = $data['schema'] ?? (\in_array($data['type'], Type::$builtinTypes, true) ? $this->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false)) : ['type' => 'string']);
717-
}
727+
$parameter = isset($description['openapi']) && $description['openapi'] instanceof Parameter ? $description['openapi'] : new Parameter(in: 'query', name: $name, style: $style, explode: $description['is_collection'] ?? false);
718728

719-
$parameters[] = new Parameter(
720-
$name,
721-
'query',
722-
$data['description'] ?? '',
723-
$data['required'] ?? false,
724-
$data['openapi']['deprecated'] ?? false,
725-
$data['openapi']['allowEmptyValue'] ?? true,
726-
$schema,
727-
'array' === $schema['type'] && \in_array(
728-
$data['type'],
729-
[Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT],
730-
true
731-
) ? 'deepObject' : 'form',
732-
$data['openapi']['explode'] ?? ('array' === $schema['type']),
733-
$data['openapi']['allowReserved'] ?? false,
734-
$data['openapi']['example'] ?? null,
735-
isset(
736-
$data['openapi']['examples']
737-
) ? new \ArrayObject($data['openapi']['examples']) : null
738-
);
729+
if ('' === $parameter->getDescription() && ($str = $description['description'] ?? '')) {
730+
$parameter = $parameter->withDescription($str);
731+
}
732+
733+
if (false === $parameter->getRequired() && false !== ($required = $description['required'] ?? false)) {
734+
$parameter = $parameter->withRequired($required);
739735
}
736+
737+
return $parameter->withSchema($schema);
740738
}
741739

742-
return $parameters;
740+
trigger_deprecation('api-platform/core', '4.0', \sprintf('Not using "%s" on the "openapi" field of the %s::getDescription() (%s) is deprecated.', Parameter::class, $filter, $shortName));
741+
if ($this->jsonSchemaTypeFactory) {
742+
$schema = $description['schema'] ?? (\in_array($description['type'], Type::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false), 'openapi') : ['type' => 'string']);
743+
} else {
744+
$schema = $description['schema'] ?? (\in_array($description['type'], Type::$builtinTypes, true) ? $this->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false)) : ['type' => 'string']);
745+
}
746+
747+
return new Parameter(
748+
$name,
749+
'query',
750+
$description['description'] ?? '',
751+
$description['required'] ?? false,
752+
$description['openapi']['deprecated'] ?? false,
753+
$description['openapi']['allowEmptyValue'] ?? true,
754+
$schema,
755+
'array' === $schema['type'] && \in_array(
756+
$description['type'],
757+
[Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT],
758+
true
759+
) ? 'deepObject' : 'form',
760+
$description['openapi']['explode'] ?? ('array' === $schema['type']),
761+
$description['openapi']['allowReserved'] ?? false,
762+
$description['openapi']['example'] ?? null,
763+
isset(
764+
$description['openapi']['examples']
765+
) ? new \ArrayObject($description['openapi']['examples']) : null
766+
);
743767
}
744768

745769
private function getPaginationParameters(CollectionOperationInterface|HttpOperation $operation): array

src/OpenApi/Tests/Factory/OpenApiFactoryTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,8 @@ public function testInvoke(): void
888888
'type' => 'string',
889889
'enum' => ['asc', 'desc'],
890890
]),
891-
]
891+
],
892+
deprecated: false
892893
), $filteredPath->getGet());
893894

894895
$paginatedPath = $paths->getPath('/paginated');
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource;
15+
16+
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
17+
use ApiPlatform\Doctrine\Orm\State\CollectionProvider;
18+
use ApiPlatform\Doctrine\Orm\State\Options;
19+
use ApiPlatform\Metadata\ApiFilter;
20+
use ApiPlatform\Metadata\GetCollection;
21+
use ApiPlatform\Metadata\QueryParameter;
22+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilterWithStateOptionsEntity;
23+
24+
#[GetCollection(
25+
uriTemplate: 'filter_with_state_options',
26+
stateOptions: new Options(entityClass: FilterWithStateOptionsEntity::class),
27+
parameters: ['date' => new QueryParameter(filter: 'filter_with_state_options_date', property: 'dummyDate')],
28+
provider: CollectionProvider::class
29+
)]
30+
#[ApiFilter(DateFilter::class, alias: 'filter_with_state_options_date', properties: ['dummyDate' => DateFilter::EXCLUDE_NULL])]
31+
final class FilterWithStateOptions
32+
{
33+
public function __construct(public readonly string $id, public readonly \DateTimeImmutable $dummyDate, public readonly string $name)
34+
{
35+
}
36+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;
15+
16+
use Doctrine\ORM\Mapping as ORM;
17+
18+
#[ORM\Entity]
19+
class FilterWithStateOptionsEntity
20+
{
21+
public function __construct(
22+
#[ORM\Column(type: 'integer')]
23+
#[ORM\Id]
24+
#[ORM\GeneratedValue(strategy: 'AUTO')]
25+
public ?int $id = null,
26+
#[ORM\Column(type: 'date_immutable', nullable: true)]
27+
public ?\DateTimeImmutable $dummyDate = null,
28+
#[ORM\Column(type: 'string', nullable: true)]
29+
public ?string $name = null,
30+
) {
31+
}
32+
}

0 commit comments

Comments
 (0)