Skip to content

feat: iri search filter #7079

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
62 changes: 62 additions & 0 deletions src/Doctrine/Orm/Filter/IriFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\Filter;

use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\Metadata\ParameterProviderFilterInterface;
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
use ApiPlatform\State\Provider\IriConverterParameterProvider;
use Doctrine\ORM\QueryBuilder;

class IriFilter implements FilterInterface, OpenApiParameterFilterInterface, ParameterProviderFilterInterface
{
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void

Check warning on line 27 in src/Doctrine/Orm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Orm/Filter/IriFilter.php#L27

Added line #L27 was not covered by tests
{
if (!$parameter = $context['parameter'] ?? null) {
return;

Check warning on line 30 in src/Doctrine/Orm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Orm/Filter/IriFilter.php#L29-L30

Added lines #L29 - L30 were not covered by tests
}

$value = $parameter->getValue();
if (!\is_array($value)) {
$value = [$value];

Check warning on line 35 in src/Doctrine/Orm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Orm/Filter/IriFilter.php#L33-L35

Added lines #L33 - L35 were not covered by tests
}

$property = $parameter->getProperty();
$alias = $queryBuilder->getRootAliases()[0];
$parameterName = $queryNameGenerator->generateParameterName($property);

Check warning on line 40 in src/Doctrine/Orm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Orm/Filter/IriFilter.php#L38-L40

Added lines #L38 - L40 were not covered by tests

$queryBuilder
->join(\sprintf('%s.%s', $alias, $property), $parameterName)
->andWhere(\sprintf('%s IN(:%s)', $parameterName, $parameterName))
->setParameter($parameterName, $value);

Check warning on line 45 in src/Doctrine/Orm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Orm/Filter/IriFilter.php#L42-L45

Added lines #L42 - L45 were not covered by tests
}

public static function getParameterProvider(): string
{
return IriConverterParameterProvider::class;
}

public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
{
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
}

public function getDescription(string $resourceClass): array
{
return [];
}
}
Empty file.
7 changes: 7 additions & 0 deletions src/Metadata/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@
return $this->extraProperties['_api_values'] ?? $default;
}

public function setValue(mixed $value): static

Check warning on line 130 in src/Metadata/Parameter.php

View check run for this annotation

Codecov / codecov/patch

src/Metadata/Parameter.php#L130

Added line #L130 was not covered by tests
{
$this->extraProperties['_api_values'] = $value;

Check warning on line 132 in src/Metadata/Parameter.php

View check run for this annotation

Codecov / codecov/patch

src/Metadata/Parameter.php#L132

Added line #L132 was not covered by tests

return $this;

Check warning on line 134 in src/Metadata/Parameter.php

View check run for this annotation

Codecov / codecov/patch

src/Metadata/Parameter.php#L134

Added line #L134 was not covered by tests
}

/**
* @return array<string, mixed>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ private function getDefaultParameters(Operation $operation, string $resourceClas
['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass);
$parameters = $operation->getParameters() ?? new Parameters();
foreach ($parameters as $key => $parameter) {
if (null === $parameter->getProvider() && (($f = $parameter->getFilter()) && $f instanceof ParameterProviderFilterInterface)) {
$parameters->add($key, $parameter->withProvider($f->getParameterProvider()));
Comment on lines +120 to +121
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (null === $parameter->getProvider() && (($f = $parameter->getFilter()) && $f instanceof ParameterProviderFilterInterface)) {
$parameters->add($key, $parameter->withProvider($f->getParameterProvider()));
if (null === $parameter->getProvider() && (($filter = $parameter->getFilter()) && $f instanceof ParameterProviderFilterInterface)) {
$parameters->add($key, $parameter->withProvider($filter->getParameterProvider()));

}

if (':property' === $key) {
foreach ($propertyNames as $property) {
$converted = $this->nameConverter?->denormalize($property) ?? $property;
Expand All @@ -131,7 +135,7 @@ private function getDefaultParameters(Operation $operation, string $resourceClas

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

if (str_contains($key, ':property') || (($f = $parameter->getFilter()) && is_a($f, PropertiesAwareInterface::class, true)) || $parameter instanceof PropertiesAwareInterface) {
if (str_contains($key, ':property') && ((($f = $parameter->getFilter()) && is_a($f, PropertiesAwareInterface::class, true)) || $parameter instanceof PropertiesAwareInterface)) {
$p = [];
foreach ($propertyNames as $prop) {
$p[$this->nameConverter?->denormalize($prop) ?? $prop] = $prop;
Expand Down
52 changes: 52 additions & 0 deletions src/State/Provider/IriConverterParameterProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\State\Provider;

use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\State\ParameterNotFound;
use ApiPlatform\State\ParameterProviderInterface;

/**
* @author Vincent Amstoutz
*/
final readonly class IriConverterParameterProvider implements ParameterProviderInterface
{
public function __construct(

Check warning on line 27 in src/State/Provider/IriConverterParameterProvider.php

View check run for this annotation

Codecov / codecov/patch

src/State/Provider/IriConverterParameterProvider.php#L27

Added line #L27 was not covered by tests
private IriConverterInterface $iriConverter,
) {
}

Check warning on line 30 in src/State/Provider/IriConverterParameterProvider.php

View check run for this annotation

Codecov / codecov/patch

src/State/Provider/IriConverterParameterProvider.php#L30

Added line #L30 was not covered by tests

public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation

Check warning on line 32 in src/State/Provider/IriConverterParameterProvider.php

View check run for this annotation

Codecov / codecov/patch

src/State/Provider/IriConverterParameterProvider.php#L32

Added line #L32 was not covered by tests
{
$operation = $context['operation'] ?? null;
if (!($value = $parameter->getValue()) || $value instanceof ParameterNotFound) {
return $operation;

Check warning on line 36 in src/State/Provider/IriConverterParameterProvider.php

View check run for this annotation

Codecov / codecov/patch

src/State/Provider/IriConverterParameterProvider.php#L34-L36

Added lines #L34 - L36 were not covered by tests
}

if (!\is_array($value)) {
$value = [$value];

Check warning on line 40 in src/State/Provider/IriConverterParameterProvider.php

View check run for this annotation

Codecov / codecov/patch

src/State/Provider/IriConverterParameterProvider.php#L39-L40

Added lines #L39 - L40 were not covered by tests
}

$entities = [];
foreach ($value as $v) {
$entities[] = $this->iriConverter->getResourceFromIri($v, ['fetch_data' => false]);

Check warning on line 45 in src/State/Provider/IriConverterParameterProvider.php

View check run for this annotation

Codecov / codecov/patch

src/State/Provider/IriConverterParameterProvider.php#L43-L45

Added lines #L43 - L45 were not covered by tests
}

$parameter->setValue($entities);

Check warning on line 48 in src/State/Provider/IriConverterParameterProvider.php

View check run for this annotation

Codecov / codecov/patch

src/State/Provider/IriConverterParameterProvider.php#L48

Added line #L48 was not covered by tests

return $operation;

Check warning on line 50 in src/State/Provider/IriConverterParameterProvider.php

View check run for this annotation

Codecov / codecov/patch

src/State/Provider/IriConverterParameterProvider.php#L50

Added line #L50 was not covered by tests
}
}
6 changes: 6 additions & 0 deletions src/Symfony/Bundle/Resources/config/state/provider.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,11 @@
<argument type="service" id="api_platform.state_provider.parameter.inner" />
<argument type="tagged_locator" tag="api_platform.parameter_provider" index-by="key" />
</service>

<service id="api_platform.state_provider.parameter.iri_converter" class="ApiPlatform\State\Provider\IriConverterParameterProvider" public="false">
<argument type="service" id="api_platform.iri_converter"/>

<tag name="api_platform.parameter_provider" key="ApiPlatform\State\Provider\IriConverterParameterProvider" priority="-895" />
</service>
</services>
</container>
60 changes: 60 additions & 0 deletions tests/Fixtures/TestBundle/Document/Chicken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\Tests\Fixtures\TestBundle\Document;

use ApiPlatform\Metadata\Get;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;

#[ODM\Document]
#[Get]
class Chicken
{
#[ODM\Id]
private string $id;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private string $id;
private ?string $id = null;


#[ODM\Field(type: 'string')]
private string $name;

#[ODM\ReferenceOne(targetDocument: ChickenCoop::class, inversedBy: 'chickens')]
private ?ChickenCoop $chickenCoop = null;

public function getId(): ?string
{
return $this->id;
}

public function getName(): ?string
{
return $this->name;
}

public function setName(string $name): self
{
$this->name = $name;

return $this;
}

public function getChickenCoop(): ?ChickenCoop
{
return $this->chickenCoop;
}

public function setChickenCoop(?ChickenCoop $chickenCoop): self
{
$this->chickenCoop = $chickenCoop;

return $this;
}
}
72 changes: 72 additions & 0 deletions tests/Fixtures/TestBundle/Document/ChickenCoop.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\Tests\Fixtures\TestBundle\Document;

use ApiPlatform\Doctrine\Orm\Filter\IriFilter;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
use ApiPlatform\Doctrine\Orm\Filter\IriFilter;
use ApiPlatform\Doctrine\Odm\Filter\IriFilter;

use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Chicken;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Chicken;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\Chicken;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;

#[ODM\Document]
#[GetCollection(normalizationContext: ['hydra_prefix' => false], parameters: ['chickens' => new QueryParameter(filter: new IriFilter())])]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[GetCollection(normalizationContext: ['hydra_prefix' => false], parameters: ['chickens' => new QueryParameter(filter: new IriFilter())])]
#[GetCollection(
normalizationContext: ['hydra_prefix' => false],
parameters: ['chickens' => new QueryParameter(filter: new IriFilter())]
)]

class ChickenCoop
{
#[ODM\Id]
private ?string $id = null;

#[ODM\ReferenceMany(targetDocument: Chicken::class, mappedBy: 'chickenCoop')]
private Collection $chickens;

public function __construct()
{
$this->chickens = new ArrayCollection();
}

public function getId(): ?string
{
return $this->id;
}

/**
* @return Collection<int, Chicken>
*/
public function getChickens(): Collection
{
return $this->chickens;
}

public function addChicken(Chicken $chicken): self
{
if (!$this->chickens->contains($chicken)) {
$this->chickens[] = $chicken;
$chicken->setChickenCoop($this);

Check failure on line 56 in tests/Fixtures/TestBundle/Document/ChickenCoop.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.4)

Parameter #1 $chickenCoop of method ApiPlatform\Tests\Fixtures\TestBundle\Entity\Chicken::setChickenCoop() expects ApiPlatform\Tests\Fixtures\TestBundle\Entity\ChickenCoop|null, $this(ApiPlatform\Tests\Fixtures\TestBundle\Document\ChickenCoop) given.
}

return $this;
}

public function removeChicken(Chicken $chicken): self
{
if ($this->chickens->removeElement($chicken)) {
if ($chicken->getChickenCoop() === $this) {

Check failure on line 65 in tests/Fixtures/TestBundle/Document/ChickenCoop.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.4)

Strict comparison using === between ApiPlatform\Tests\Fixtures\TestBundle\Entity\ChickenCoop|null and $this(ApiPlatform\Tests\Fixtures\TestBundle\Document\ChickenCoop) will always evaluate to false.
$chicken->setChickenCoop(null);
}
}

return $this;
}
}
2 changes: 1 addition & 1 deletion tests/Fixtures/TestBundle/Document/Company.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource]
#[GetCollection]
#[GetCollection()]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[GetCollection()]
#[GetCollection]

#[Get]
#[Post]
#[ApiResource(uriTemplate: '/employees/{employeeId}/rooms/{roomId}/company/{companyId}', uriVariables: ['employeeId' => ['from_class' => Employee::class, 'from_property' => 'company']])]
Expand Down
63 changes: 63 additions & 0 deletions tests/Fixtures/TestBundle/Entity/Chicken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\Tests\Fixtures\TestBundle\Entity;

use ApiPlatform\Metadata\Get;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[Get()]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[Get()]
#[Get]

class Chicken
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;

#[ORM\Column(type: 'string', length: 255)]
private string $name;

#[ORM\ManyToOne(targetEntity: ChickenCoop::class, inversedBy: 'chickens')]
#[ORM\JoinColumn(nullable: false)]
private ChickenCoop $chickenCoop;

public function getId(): ?int
{
return $this->id;
}

public function getName(): ?string
{
return $this->name;
}

public function setName(string $name): self
{
$this->name = $name;

return $this;
}

public function getChickenCoop(): ?ChickenCoop
{
return $this->chickenCoop;
}

public function setChickenCoop(?ChickenCoop $chickenCoop): self
{
$this->chickenCoop = $chickenCoop;

return $this;
}
}
Loading
Loading