Skip to content

Commit 9342898

Browse files
committed
feat: iri search filter
continues the work at api-platform#6865
1 parent 066c18e commit 9342898

File tree

14 files changed

+494
-3
lines changed

14 files changed

+494
-3
lines changed

src/Doctrine/Orm/Filter/ExactSearchFilter.php

Whitespace-only changes.

src/Doctrine/Orm/Filter/IriFilter.php

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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\Doctrine\Orm\Filter;
15+
16+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
17+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
18+
use ApiPlatform\Metadata\Operation;
19+
use ApiPlatform\Metadata\Parameter;
20+
use ApiPlatform\Metadata\ParameterProviderFilterInterface;
21+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
22+
use ApiPlatform\State\Provider\IriConverterParameterProvider;
23+
use Doctrine\ORM\QueryBuilder;
24+
25+
class IriFilter implements FilterInterface, OpenApiParameterFilterInterface, ParameterProviderFilterInterface
26+
{
27+
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
28+
{
29+
if (!$parameter = $context['parameter'] ?? null) {
30+
return;
31+
}
32+
33+
$value = $parameter->getValue();
34+
if (!\is_array($value)) {
35+
$value = [$value];
36+
}
37+
38+
$property = $parameter->getProperty();
39+
$alias = $queryBuilder->getRootAliases()[0];
40+
$parameterName = $queryNameGenerator->generateParameterName($property);
41+
42+
$queryBuilder
43+
->join(\sprintf('%s.%s', $alias, $property), $parameterName)
44+
->andWhere(\sprintf('%s IN(:%s)', $parameterName, $parameterName))
45+
->setParameter($parameterName, $value);
46+
}
47+
48+
public static function getParameterProvider(): string
49+
{
50+
return IriConverterParameterProvider::class;
51+
}
52+
53+
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
54+
{
55+
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
56+
}
57+
58+
public function getDescription(string $resourceClass): array
59+
{
60+
return [];
61+
}
62+
}

src/Doctrine/Orm/Filter/PartialSearchFilter.php

Whitespace-only changes.

src/Metadata/Parameter.php

+7
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@ public function getValue(mixed $default = new ParameterNotFound()): mixed
127127
return $this->extraProperties['_api_values'] ?? $default;
128128
}
129129

130+
public function setValue(mixed $value): static
131+
{
132+
$this->extraProperties['_api_values'] = $value;
133+
134+
return $this;
135+
}
136+
130137
/**
131138
* @return array<string, mixed>
132139
*/

src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ private function getDefaultParameters(Operation $operation, string $resourceClas
117117
['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass);
118118
$parameters = $operation->getParameters() ?? new Parameters();
119119
foreach ($parameters as $key => $parameter) {
120+
if (null === $parameter->getProvider() && (($f = $parameter->getFilter()) && $f instanceof ParameterProviderFilterInterface)) {
121+
$parameters->add($key, $parameter->withProvider($f->getParameterProvider()));
122+
}
123+
120124
if (':property' === $key) {
121125
foreach ($propertyNames as $property) {
122126
$converted = $this->nameConverter?->denormalize($property) ?? $property;
@@ -131,7 +135,7 @@ private function getDefaultParameters(Operation $operation, string $resourceClas
131135

132136
$key = $parameter->getKey() ?? $key;
133137

134-
if (str_contains($key, ':property') || (($f = $parameter->getFilter()) && is_a($f, PropertiesAwareInterface::class, true)) || $parameter instanceof PropertiesAwareInterface) {
138+
if (str_contains($key, ':property') && ((($f = $parameter->getFilter()) && is_a($f, PropertiesAwareInterface::class, true)) || $parameter instanceof PropertiesAwareInterface)) {
135139
$p = [];
136140
foreach ($propertyNames as $prop) {
137141
$p[$this->nameConverter?->denormalize($prop) ?? $prop] = $prop;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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\State\Provider;
15+
16+
use ApiPlatform\Metadata\IriConverterInterface;
17+
use ApiPlatform\Metadata\Operation;
18+
use ApiPlatform\Metadata\Parameter;
19+
use ApiPlatform\State\ParameterNotFound;
20+
use ApiPlatform\State\ParameterProviderInterface;
21+
22+
/**
23+
* @author Vincent Amstoutz
24+
*/
25+
final readonly class IriConverterParameterProvider implements ParameterProviderInterface
26+
{
27+
public function __construct(
28+
private IriConverterInterface $iriConverter,
29+
) {
30+
}
31+
32+
public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation
33+
{
34+
$operation = $context['operation'] ?? null;
35+
if (!($value = $parameter->getValue()) || $value instanceof ParameterNotFound) {
36+
return $operation;
37+
}
38+
39+
if (!\is_array($value)) {
40+
$value = [$value];
41+
}
42+
43+
$entities = [];
44+
foreach ($value as $v) {
45+
$entities[] = $this->iriConverter->getResourceFromIri($v, ['fetch_data' => false]);
46+
}
47+
48+
$parameter->setValue($entities);
49+
50+
return $operation;
51+
}
52+
}

src/Symfony/Bundle/Resources/config/state/provider.xml

+6
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,11 @@
4242
<argument type="service" id="api_platform.state_provider.parameter.inner" />
4343
<argument type="tagged_locator" tag="api_platform.parameter_provider" index-by="key" />
4444
</service>
45+
46+
<service id="api_platform.state_provider.parameter.iri_converter" class="ApiPlatform\State\Provider\IriConverterParameterProvider" public="false">
47+
<argument type="service" id="api_platform.iri_converter"/>
48+
49+
<tag name="api_platform.parameter_provider" key="ApiPlatform\State\Provider\IriConverterParameterProvider" priority="-895" />
50+
</service>
4551
</services>
4652
</container>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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\Metadata\Get;
17+
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
18+
19+
#[ODM\Document]
20+
#[Get]
21+
class Chicken
22+
{
23+
#[ODM\Id]
24+
private string $id;
25+
26+
#[ODM\Field(type: 'string')]
27+
private string $name;
28+
29+
#[ODM\ReferenceOne(targetDocument: ChickenCoop::class, inversedBy: 'chickens')]
30+
private ?ChickenCoop $chickenCoop = null;
31+
32+
public function getId(): ?string
33+
{
34+
return $this->id;
35+
}
36+
37+
public function getName(): ?string
38+
{
39+
return $this->name;
40+
}
41+
42+
public function setName(string $name): self
43+
{
44+
$this->name = $name;
45+
46+
return $this;
47+
}
48+
49+
public function getChickenCoop(): ?ChickenCoop
50+
{
51+
return $this->chickenCoop;
52+
}
53+
54+
public function setChickenCoop(?ChickenCoop $chickenCoop): self
55+
{
56+
$this->chickenCoop = $chickenCoop;
57+
58+
return $this;
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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\Orm\Filter\IriFilter;
17+
use ApiPlatform\Metadata\GetCollection;
18+
use ApiPlatform\Metadata\QueryParameter;
19+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Chicken;
20+
use Doctrine\Common\Collections\ArrayCollection;
21+
use Doctrine\Common\Collections\Collection;
22+
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
23+
24+
#[ODM\Document]
25+
#[GetCollection(normalizationContext: ['hydra_prefix' => false], parameters: ['chickens' => new QueryParameter(filter: new IriFilter())])]
26+
class ChickenCoop
27+
{
28+
#[ODM\Id]
29+
private ?string $id = null;
30+
31+
#[ODM\ReferenceMany(targetDocument: Chicken::class, mappedBy: 'chickenCoop')]
32+
private Collection $chickens;
33+
34+
public function __construct()
35+
{
36+
$this->chickens = new ArrayCollection();
37+
}
38+
39+
public function getId(): ?string
40+
{
41+
return $this->id;
42+
}
43+
44+
/**
45+
* @return Collection<int, Chicken>
46+
*/
47+
public function getChickens(): Collection
48+
{
49+
return $this->chickens;
50+
}
51+
52+
public function addChicken(Chicken $chicken): self
53+
{
54+
if (!$this->chickens->contains($chicken)) {
55+
$this->chickens[] = $chicken;
56+
$chicken->setChickenCoop($this);
57+
}
58+
59+
return $this;
60+
}
61+
62+
public function removeChicken(Chicken $chicken): self
63+
{
64+
if ($this->chickens->removeElement($chicken)) {
65+
if ($chicken->getChickenCoop() === $this) {
66+
$chicken->setChickenCoop(null);
67+
}
68+
}
69+
70+
return $this;
71+
}
72+
}

tests/Fixtures/TestBundle/Document/Company.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
use Symfony\Component\Serializer\Annotation\Groups;
2323

2424
#[ApiResource]
25-
#[GetCollection]
25+
#[GetCollection()]
2626
#[Get]
2727
#[Post]
2828
#[ApiResource(uriTemplate: '/employees/{employeeId}/rooms/{roomId}/company/{companyId}', uriVariables: ['employeeId' => ['from_class' => Employee::class, 'from_property' => 'company']])]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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\Metadata\Get;
17+
use Doctrine\ORM\Mapping as ORM;
18+
19+
#[ORM\Entity]
20+
#[Get()]
21+
class Chicken
22+
{
23+
#[ORM\Id]
24+
#[ORM\GeneratedValue]
25+
#[ORM\Column(type: 'integer')]
26+
private ?int $id = null;
27+
28+
#[ORM\Column(type: 'string', length: 255)]
29+
private string $name;
30+
31+
#[ORM\ManyToOne(targetEntity: ChickenCoop::class, inversedBy: 'chickens')]
32+
#[ORM\JoinColumn(nullable: false)]
33+
private ChickenCoop $chickenCoop;
34+
35+
public function getId(): ?int
36+
{
37+
return $this->id;
38+
}
39+
40+
public function getName(): ?string
41+
{
42+
return $this->name;
43+
}
44+
45+
public function setName(string $name): self
46+
{
47+
$this->name = $name;
48+
49+
return $this;
50+
}
51+
52+
public function getChickenCoop(): ?ChickenCoop
53+
{
54+
return $this->chickenCoop;
55+
}
56+
57+
public function setChickenCoop(?ChickenCoop $chickenCoop): self
58+
{
59+
$this->chickenCoop = $chickenCoop;
60+
61+
return $this;
62+
}
63+
}

0 commit comments

Comments
 (0)