Skip to content

Commit 9b7ace5

Browse files
authored
fix(graphql): build filter args from parameters (#8347)
1 parent 3caae14 commit 9b7ace5

10 files changed

Lines changed: 932 additions & 194 deletions

File tree

src/Doctrine/Odm/Filter/SortFilter.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
2222
use ApiPlatform\Metadata\Operation;
2323
use ApiPlatform\Metadata\Parameter;
24+
use ApiPlatform\Metadata\SortFilterInterface;
2425
use Doctrine\ODM\MongoDB\Aggregation\Builder;
2526

2627
/**
@@ -33,7 +34,7 @@
3334
*
3435
* @author Antoine Bluchet <soyuka@gmail.com>
3536
*/
36-
final class SortFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
37+
final class SortFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface, SortFilterInterface
3738
{
3839
use BackwardCompatibleFilterDescriptionTrait;
3940
use NestedPropertyHelperTrait;

src/Doctrine/Orm/Filter/SortFilter.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
2323
use ApiPlatform\Metadata\Operation;
2424
use ApiPlatform\Metadata\Parameter;
25+
use ApiPlatform\Metadata\SortFilterInterface;
2526
use Doctrine\ORM\Query\Expr\Join;
2627
use Doctrine\ORM\QueryBuilder;
2728

@@ -35,7 +36,7 @@
3536
*
3637
* @author Antoine Bluchet <soyuka@gmail.com>
3738
*/
38-
final class SortFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
39+
final class SortFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface, SortFilterInterface
3940
{
4041
use BackwardCompatibleFilterDescriptionTrait;
4142
use NestedPropertyHelperTrait;

src/GraphQl/Type/FieldsBuilder.php

Lines changed: 311 additions & 191 deletions
Large diffs are not rendered by default.

src/Laravel/Eloquent/Filter/OrderFilter.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
1717
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
1818
use ApiPlatform\Metadata\Parameter;
19+
use ApiPlatform\Metadata\SortFilterInterface;
1920
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
2021
use Illuminate\Database\Eloquent\Builder;
2122
use Illuminate\Database\Eloquent\Model;
2223
use Illuminate\Database\Eloquent\Relations\BelongsTo;
2324
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
2425

25-
final class OrderFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
26+
final class OrderFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface, SortFilterInterface
2627
{
2728
use QueryPropertyTrait;
2829

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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\Metadata;
15+
16+
/**
17+
* Marks a filter that sorts a collection by one or more properties.
18+
*
19+
* Backend-agnostic so consumers can recognize a sort filter without depending
20+
* on a persistence layer: GraphQL, for instance, exposes such a parameter as an
21+
* ordered list of single-property inputs to preserve multi-key ordering, which
22+
* an (unordered) input object cannot express.
23+
*/
24+
interface SortFilterInterface
25+
{
26+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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\Document;
15+
16+
use ApiPlatform\Doctrine\Odm\Filter\ComparisonFilter;
17+
use ApiPlatform\Doctrine\Odm\Filter\ExactFilter;
18+
use ApiPlatform\Doctrine\Odm\Filter\PartialSearchFilter;
19+
use ApiPlatform\Doctrine\Odm\Filter\SortFilter;
20+
use ApiPlatform\Metadata\ApiResource;
21+
use ApiPlatform\Metadata\GraphQl\Query;
22+
use ApiPlatform\Metadata\GraphQl\QueryCollection;
23+
use ApiPlatform\Metadata\QueryParameter;
24+
use Doctrine\Common\Collections\ArrayCollection;
25+
use Doctrine\Common\Collections\Collection;
26+
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
27+
use Symfony\Component\Serializer\Attribute as Serializer;
28+
use Symfony\Component\TypeInfo\Type\BuiltinType;
29+
use Symfony\Component\TypeInfo\TypeIdentifier;
30+
31+
/**
32+
* ODM mirror of the QueryParameter-based GraphQL parity fixture.
33+
*/
34+
#[ApiResource(
35+
normalizationContext: ['groups' => ['graphql_filtered']],
36+
graphQlOperations: [
37+
new Query(),
38+
new QueryCollection(
39+
parameters: [
40+
'name' => new QueryParameter(filter: new ExactFilter()),
41+
'colors.prop' => new QueryParameter(filter: new PartialSearchFilter(), property: 'colors.prop'),
42+
'colors.price' => new QueryParameter(filter: new ComparisonFilter(new ExactFilter()), property: 'colors.price', nativeType: new BuiltinType(TypeIdentifier::INT)),
43+
'order[:property]' => new QueryParameter(filter: new SortFilter()),
44+
],
45+
),
46+
],
47+
)]
48+
#[ODM\Document]
49+
class GraphQlFilteredResource
50+
{
51+
#[ODM\Id(strategy: 'INCREMENT', type: 'int')]
52+
#[Serializer\Groups(['graphql_filtered'])]
53+
private ?int $id = null;
54+
55+
#[ODM\Field(type: 'string')]
56+
#[Serializer\Groups(['graphql_filtered'])]
57+
private string $name = '';
58+
59+
/**
60+
* @var Collection<int, GraphQlFilteredResourceColor>
61+
*/
62+
#[ODM\ReferenceMany(targetDocument: GraphQlFilteredResourceColor::class, mappedBy: 'resource')]
63+
#[Serializer\Groups(['graphql_filtered'])]
64+
private Collection $colors;
65+
66+
public function __construct()
67+
{
68+
$this->colors = new ArrayCollection();
69+
}
70+
71+
public function getId(): ?int
72+
{
73+
return $this->id;
74+
}
75+
76+
public function getName(): string
77+
{
78+
return $this->name;
79+
}
80+
81+
public function setName(string $name): void
82+
{
83+
$this->name = $name;
84+
}
85+
86+
/**
87+
* @return Collection<int, GraphQlFilteredResourceColor>
88+
*/
89+
public function getColors(): Collection
90+
{
91+
return $this->colors;
92+
}
93+
94+
public function setColors(Collection $colors): void
95+
{
96+
$this->colors = $colors;
97+
}
98+
99+
public function addColor(GraphQlFilteredResourceColor $color): void
100+
{
101+
if (!$this->colors->contains($color)) {
102+
$this->colors->add($color);
103+
$color->setResource($this);
104+
}
105+
}
106+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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\Document;
15+
16+
use ApiPlatform\Doctrine\Odm\Filter\ComparisonFilter;
17+
use ApiPlatform\Doctrine\Odm\Filter\ExactFilter;
18+
use ApiPlatform\Doctrine\Odm\Filter\PartialSearchFilter;
19+
use ApiPlatform\Metadata\ApiResource;
20+
use ApiPlatform\Metadata\GraphQl\Query;
21+
use ApiPlatform\Metadata\GraphQl\QueryCollection;
22+
use ApiPlatform\Metadata\QueryParameter;
23+
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
24+
use Symfony\Component\Serializer\Attribute as Serializer;
25+
use Symfony\Component\TypeInfo\Type\BuiltinType;
26+
use Symfony\Component\TypeInfo\TypeIdentifier;
27+
28+
/**
29+
* ODM mirror of the QueryParameter-based "color" collection resource.
30+
*/
31+
#[ApiResource(
32+
normalizationContext: ['groups' => ['graphql_filtered']],
33+
graphQlOperations: [
34+
new Query(),
35+
new QueryCollection(
36+
parameters: [
37+
'prop' => new QueryParameter(filter: new PartialSearchFilter(), property: 'prop'),
38+
'price' => new QueryParameter(filter: new ComparisonFilter(new ExactFilter()), property: 'price', nativeType: new BuiltinType(TypeIdentifier::INT)),
39+
],
40+
),
41+
],
42+
)]
43+
#[ODM\Document]
44+
class GraphQlFilteredResourceColor
45+
{
46+
#[ODM\Id(strategy: 'INCREMENT', type: 'int')]
47+
#[Serializer\Groups(['graphql_filtered'])]
48+
private ?int $id = null;
49+
50+
#[ODM\ReferenceOne(targetDocument: GraphQlFilteredResource::class, inversedBy: 'colors', storeAs: 'id')]
51+
private ?GraphQlFilteredResource $resource = null;
52+
53+
#[ODM\Field(type: 'string')]
54+
#[Serializer\Groups(['graphql_filtered'])]
55+
private string $prop = '';
56+
57+
#[ODM\Field(type: 'int')]
58+
#[Serializer\Groups(['graphql_filtered'])]
59+
private int $price = 0;
60+
61+
public function getId(): ?int
62+
{
63+
return $this->id;
64+
}
65+
66+
public function getResource(): ?GraphQlFilteredResource
67+
{
68+
return $this->resource;
69+
}
70+
71+
public function setResource(?GraphQlFilteredResource $resource): void
72+
{
73+
$this->resource = $resource;
74+
}
75+
76+
public function getProp(): string
77+
{
78+
return $this->prop;
79+
}
80+
81+
public function setProp(string $prop): void
82+
{
83+
$this->prop = $prop;
84+
}
85+
86+
public function getPrice(): int
87+
{
88+
return $this->price;
89+
}
90+
91+
public function setPrice(int $price): void
92+
{
93+
$this->price = $price;
94+
}
95+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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 ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter;
17+
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
18+
use ApiPlatform\Doctrine\Orm\Filter\PartialSearchFilter;
19+
use ApiPlatform\Doctrine\Orm\Filter\SortFilter;
20+
use ApiPlatform\Metadata\ApiResource;
21+
use ApiPlatform\Metadata\GraphQl\Query;
22+
use ApiPlatform\Metadata\GraphQl\QueryCollection;
23+
use ApiPlatform\Metadata\QueryParameter;
24+
use Doctrine\Common\Collections\ArrayCollection;
25+
use Doctrine\Common\Collections\Collection;
26+
use Doctrine\ORM\Mapping as ORM;
27+
use Symfony\Component\Serializer\Attribute as Serializer;
28+
use Symfony\Component\TypeInfo\Type\BuiltinType;
29+
use Symfony\Component\TypeInfo\TypeIdentifier;
30+
31+
/**
32+
* Parameter-based (QueryParameter) mirror of the DummyCar <-> DummyCarColor relationship,
33+
* used to prove GraphQL filter-argument parity between the canonical
34+
* Operation::getParameters() path and the legacy Operation::getFilters() path.
35+
*/
36+
#[ApiResource(
37+
normalizationContext: ['groups' => ['graphql_filtered']],
38+
graphQlOperations: [
39+
new Query(),
40+
new QueryCollection(
41+
parameters: [
42+
'name' => new QueryParameter(filter: new ExactFilter()),
43+
'colors.prop' => new QueryParameter(filter: new PartialSearchFilter(), property: 'colors.prop'),
44+
'colors.price' => new QueryParameter(filter: new ComparisonFilter(new ExactFilter()), property: 'colors.price', nativeType: new BuiltinType(TypeIdentifier::INT)),
45+
'order[:property]' => new QueryParameter(filter: new SortFilter()),
46+
],
47+
),
48+
],
49+
)]
50+
#[ORM\Entity]
51+
class GraphQlFilteredResource
52+
{
53+
#[ORM\Id]
54+
#[ORM\GeneratedValue]
55+
#[ORM\Column(type: 'integer')]
56+
#[Serializer\Groups(['graphql_filtered'])]
57+
private ?int $id = null;
58+
59+
#[ORM\Column(type: 'string')]
60+
#[Serializer\Groups(['graphql_filtered'])]
61+
private string $name = '';
62+
63+
/**
64+
* @var Collection<int, GraphQlFilteredResourceColor>
65+
*/
66+
#[ORM\OneToMany(targetEntity: GraphQlFilteredResourceColor::class, mappedBy: 'resource')]
67+
#[Serializer\Groups(['graphql_filtered'])]
68+
private Collection $colors;
69+
70+
public function __construct()
71+
{
72+
$this->colors = new ArrayCollection();
73+
}
74+
75+
public function getId(): ?int
76+
{
77+
return $this->id;
78+
}
79+
80+
public function getName(): string
81+
{
82+
return $this->name;
83+
}
84+
85+
public function setName(string $name): void
86+
{
87+
$this->name = $name;
88+
}
89+
90+
/**
91+
* @return Collection<int, GraphQlFilteredResourceColor>
92+
*/
93+
public function getColors(): Collection
94+
{
95+
return $this->colors;
96+
}
97+
98+
public function setColors(Collection $colors): void
99+
{
100+
$this->colors = $colors;
101+
}
102+
103+
public function addColor(GraphQlFilteredResourceColor $color): void
104+
{
105+
if (!$this->colors->contains($color)) {
106+
$this->colors->add($color);
107+
$color->setResource($this);
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)