Skip to content

Commit a8fdb30

Browse files
soyukasimondaigre
andauthored
Merge pull request #6758 from soyuka/merge342
Co-authored-by: Simon <1218015+simondaigre@users.noreply.github.com> fix(hydra): iri template when using query parameter (#6742)
2 parents 4f65ef2 + 8d66330 commit a8fdb30

File tree

9 files changed

+230
-76
lines changed

9 files changed

+230
-76
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ Notes:
152152

153153
* [0d5f35683](https://github.com/api-platform/core/commit/0d5f356839eb6aa9f536044abe4affa736553e76) feat(laravel): laravel component (#5882)
154154

155+
## v3.4.5
156+
157+
### Bug fixes
158+
159+
* [fc8fa00a1](https://github.com/api-platform/core/commit/fc8fa00a19320b65547a60537261959c11f8e6a8) fix(hydra): iri template when using query parameter (#6742)
160+
155161
## v3.4.4
156162

157163
### Bug fixes

docs/guides/doctrine-search-filter.php

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,24 +120,21 @@ public function testGetDocumentation(): void
120120
$this->assertJsonContains([
121121
'search' => [
122122
'@type' => 'IriTemplate',
123-
'template' => '/books.jsonld{?id,title,author}',
123+
'template' => '/books.jsonld{?title,author}',
124124
'variableRepresentation' => 'BasicRepresentation',
125125
'mapping' => [
126-
[
127-
'@type' => 'IriTemplateMapping',
128-
'variable' => 'id',
129-
'property' => 'id',
130-
],
131126
[
132127
'@type' => 'IriTemplateMapping',
133128
'variable' => 'title',
134129
'property' => 'title',
130+
'required' => false,
135131
],
136132
[
137133
'@type' => 'IriTemplateMapping',
138134
'variable' => 'author',
139135
'property' => 'author',
140-
]
136+
'required' => false,
137+
],
141138
],
142139
],
143140
]);

src/Hydra/Serializer/CollectionFiltersNormalizer.php

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
use ApiPlatform\Doctrine\Orm\State\Options;
1818
use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait;
1919
use ApiPlatform\Metadata\FilterInterface;
20-
use ApiPlatform\Metadata\Parameter;
2120
use ApiPlatform\Metadata\Parameters;
2221
use ApiPlatform\Metadata\QueryParameterInterface;
2322
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
@@ -133,10 +132,9 @@ public function setNormalizer(NormalizerInterface $normalizer): void
133132
/**
134133
* Returns the content of the Hydra search property.
135134
*
136-
* @param FilterInterface[] $filters
137-
* @param array<string, Parameter> $parameters
135+
* @param FilterInterface[] $filters
138136
*/
139-
private function getSearch(string $resourceClass, array $parts, array $filters, array|Parameters|null $parameters, string $hydraPrefix): array
137+
private function getSearch(string $resourceClass, array $parts, array $filters, ?Parameters $parameters, string $hydraPrefix): array
140138
{
141139
$variables = [];
142140
$mapping = [];
@@ -153,13 +151,19 @@ private function getSearch(string $resourceClass, array $parts, array $filters,
153151
continue;
154152
}
155153

156-
if (!($property = $parameter->getProperty()) && ($filterId = $parameter->getFilter()) && ($filter = $this->getFilter($filterId))) {
157-
foreach ($filter->getDescription($resourceClass) as $variable => $description) {
158-
// This is a practice induced by PHP and is not necessary when implementing URI template
154+
if (($filterId = $parameter->getFilter()) && \is_string($filterId) && ($filter = $this->getFilter($filterId))) {
155+
$filterDescription = $filter->getDescription($resourceClass);
156+
157+
foreach ($filterDescription as $variable => $description) {
158+
// // This is a practice induced by PHP and is not necessary when implementing URI template
159159
if (str_ends_with((string) $variable, '[]')) {
160160
continue;
161161
}
162162

163+
if (($prop = $parameter->getProperty()) && ($description['property'] ?? null) !== $prop) {
164+
continue;
165+
}
166+
163167
// :property is a pattern allowed when defining parameters
164168
$k = str_replace(':property', $description['property'], $key);
165169
$variable = str_replace($description['property'], $k, $variable);
@@ -171,10 +175,12 @@ private function getSearch(string $resourceClass, array $parts, array $filters,
171175
$mapping[] = $m;
172176
}
173177

174-
continue;
178+
if ($filterDescription) {
179+
continue;
180+
}
175181
}
176182

177-
if (!$property) {
183+
if (!($property = $parameter->getProperty())) {
178184
continue;
179185
}
180186

src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,12 @@ private function addFilterMetadata(Parameter $parameter): Parameter
155155
return $parameter;
156156
}
157157

158-
if (null === $parameter->getSchema() && $filter instanceof JsonSchemaFilterInterface) {
159-
if ($schema = $filter->getSchema($parameter)) {
160-
$parameter = $parameter->withSchema($schema);
161-
}
158+
if (null === $parameter->getSchema() && $filter instanceof JsonSchemaFilterInterface && $schema = $filter->getSchema($parameter)) {
159+
$parameter = $parameter->withSchema($schema);
162160
}
163161

164-
if (null === $parameter->getOpenApi() && $filter instanceof OpenApiParameterFilterInterface) {
165-
if ($openApiParameter = $filter->getOpenApiParameters($parameter)) {
166-
$parameter = $parameter->withOpenApi($openApiParameter);
167-
}
162+
if (null === $parameter->getOpenApi() && $filter instanceof OpenApiParameterFilterInterface && ($openApiParameter = $filter->getOpenApiParameters($parameter)) && $openApiParameter instanceof OpenApiParameter) {
163+
$parameter = $parameter->withOpenApi($openApiParameter);
168164
}
169165

170166
return $parameter;
@@ -194,10 +190,6 @@ private function setDefaults(string $key, Parameter $parameter, string $resource
194190
$parameter = $parameter->withSchema($schema);
195191
}
196192

197-
if (null === $parameter->getProperty() && ($property = $description[$key]['property'] ?? null)) {
198-
$parameter = $parameter->withProperty($property);
199-
}
200-
201193
$currentKey = $key;
202194
if (null === $parameter->getProperty() && isset($properties[$key])) {
203195
$parameter = $parameter->withProperty($key);
@@ -216,10 +208,6 @@ private function setDefaults(string $key, Parameter $parameter, string $resource
216208
$parameter = $parameter->withRequired($required);
217209
}
218210

219-
if (null === $parameter->getOpenApi() && ($openApi = $description[$key]['openapi'] ?? null) && $openApi instanceof OpenApiParameter) {
220-
$parameter = $parameter->withOpenApi($openApi);
221-
}
222-
223211
return $this->addFilterMetadata($parameter);
224212
}
225213

src/OpenApi/Factory/OpenApiFactory.php

Lines changed: 86 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -261,12 +261,26 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
261261
}
262262
}
263263

264+
$entityClass = $this->getFilterClass($operation);
264265
$openapiParameters = $openapiOperation->getParameters();
265266
foreach ($operation->getParameters() ?? [] as $key => $p) {
266267
if (false === $p->getOpenApi()) {
267268
continue;
268269
}
269270

271+
if (($f = $p->getFilter()) && \is_string($f) && $this->filterLocator && $this->filterLocator->has($f)) {
272+
$filter = $this->filterLocator->get($f);
273+
foreach ($filter->getDescription($entityClass) as $name => $description) {
274+
if ($prop = $p->getProperty()) {
275+
$name = str_replace($prop, $key, $name);
276+
}
277+
278+
$openapiParameters[] = $this->getFilterParameter($name, $description, $operation->getShortName(), $f);
279+
}
280+
281+
continue;
282+
}
283+
270284
$in = $p instanceof HeaderParameterInterface ? 'header' : 'query';
271285
$defaultParameter = new Parameter($key, $in, $p->getDescription() ?? "$resourceShortName $key", $p->getRequired() ?? false, false, false, $p->getSchema() ?? ['type' => 'string']);
272286

@@ -557,57 +571,98 @@ private function getLinks(ResourceMetadataCollection $resourceMetadataCollection
557571
private function getFiltersParameters(CollectionOperationInterface|HttpOperation $operation): array
558572
{
559573
$parameters = [];
560-
561574
$resourceFilters = $operation->getFilters();
575+
$entityClass = $this->getFilterClass($operation);
576+
562577
foreach ($resourceFilters ?? [] as $filterId) {
563578
if (!$this->filterLocator->has($filterId)) {
564579
continue;
565580
}
566581

567582
$filter = $this->filterLocator->get($filterId);
568-
$entityClass = $operation->getClass();
569-
if ($options = $operation->getStateOptions()) {
570-
if ($options instanceof DoctrineOptions && $options->getEntityClass()) {
571-
$entityClass = $options->getEntityClass();
572-
}
583+
foreach ($filter->getDescription($entityClass) as $name => $description) {
584+
$parameters[] = $this->getFilterParameter($name, $description, $operation->getShortName(), $filterId);
585+
}
586+
}
573587

574-
if ($options instanceof DoctrineODMOptions && $options->getDocumentClass()) {
575-
$entityClass = $options->getDocumentClass();
576-
}
588+
return $parameters;
589+
}
590+
591+
private function getFilterClass(HttpOperation $operation): ?string
592+
{
593+
$entityClass = $operation->getClass();
594+
if ($options = $operation->getStateOptions()) {
595+
if ($options instanceof DoctrineOptions && $options->getEntityClass()) {
596+
return $options->getEntityClass();
577597
}
578598

579-
foreach ($filter->getDescription($entityClass) as $name => $data) {
580-
$schema = $data['schema'] ?? [];
599+
if ($options instanceof DoctrineODMOptions && $options->getDocumentClass()) {
600+
return $options->getDocumentClass();
601+
}
602+
}
581603

582-
if (isset($data['type']) && \in_array($data['type'] ?? null, Type::$builtinTypes, true) && !isset($schema['type'])) {
583-
$schema += $this->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false));
584-
}
604+
return $entityClass;
605+
}
585606

586-
if (!isset($schema['type'])) {
587-
$schema['type'] = 'string';
588-
}
607+
private function getFilterParameter(string $name, array $description, string $shortName, string $filter): Parameter
608+
{
609+
if (isset($description['swagger'])) {
610+
trigger_deprecation('api-platform/core', '4.0', \sprintf('Using the "swagger" field of the %s::getDescription() (%s) is deprecated.', $filter, $shortName));
611+
}
589612

590-
$style = 'array' === ($schema['type'] ?? null) && \in_array(
591-
$data['type'],
592-
[Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT],
593-
true
594-
) ? 'deepObject' : 'form';
613+
if (!isset($description['openapi']) || $description['openapi'] instanceof Parameter) {
614+
$schema = $description['schema'] ?? [];
595615

596-
$parameter = isset($data['openapi']) && $data['openapi'] instanceof Parameter ? $data['openapi'] : new Parameter(in: 'query', name: $name, style: $style, explode: $data['is_collection'] ?? false);
616+
if (isset($description['type']) && \in_array($description['type'], Type::$builtinTypes, true) && !isset($schema['type'])) {
617+
$schema += $this->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false));
618+
}
597619

598-
if ('' === $parameter->getDescription() && ($description = $data['description'] ?? '')) {
599-
$parameter = $parameter->withDescription($description);
600-
}
620+
if (!isset($schema['type'])) {
621+
$schema['type'] = 'string';
622+
}
601623

602-
if (false === $parameter->getRequired() && false !== ($required = $data['required'] ?? false)) {
603-
$parameter = $parameter->withRequired($required);
604-
}
624+
$style = 'array' === ($schema['type'] ?? null) && \in_array(
625+
$description['type'],
626+
[Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT],
627+
true
628+
) ? 'deepObject' : 'form';
629+
630+
$parameter = isset($description['openapi']) && $description['openapi'] instanceof Parameter ? $description['openapi'] : new Parameter(in: 'query', name: $name, style: $style, explode: $description['is_collection'] ?? false);
631+
632+
if ('' === $parameter->getDescription() && ($str = $description['description'] ?? '')) {
633+
$parameter = $parameter->withDescription($str);
634+
}
605635

606-
$parameters[] = $parameter->withSchema($schema);
636+
if (false === $parameter->getRequired() && false !== ($required = $description['required'] ?? false)) {
637+
$parameter = $parameter->withRequired($required);
607638
}
639+
640+
return $parameter->withSchema($schema);
608641
}
609642

610-
return $parameters;
643+
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));
644+
$schema = $description['schema'] ?? (\in_array($description['type'], Type::$builtinTypes, true) ? $this->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false)) : ['type' => 'string']);
645+
646+
return new Parameter(
647+
$name,
648+
'query',
649+
$description['description'] ?? '',
650+
$description['required'] ?? false,
651+
$description['openapi']['deprecated'] ?? false,
652+
$description['openapi']['allowEmptyValue'] ?? true,
653+
$schema,
654+
'array' === $schema['type'] && \in_array(
655+
$description['type'],
656+
[Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT],
657+
true
658+
) ? 'deepObject' : 'form',
659+
$description['openapi']['explode'] ?? ('array' === $schema['type']),
660+
$description['openapi']['allowReserved'] ?? false,
661+
$description['openapi']['example'] ?? null,
662+
isset(
663+
$description['openapi']['examples']
664+
) ? new \ArrayObject($description['openapi']['examples']) : null
665+
);
611666
}
612667

613668
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
@@ -879,7 +879,8 @@ public function testInvoke(): void
879879
'type' => 'string',
880880
'enum' => ['asc', 'desc'],
881881
]),
882-
]
882+
],
883+
deprecated: false
883884
), $filteredPath->getGet());
884885

885886
$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)