Skip to content

feat(doctrine): new iri search filters #7121

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
62 changes: 62 additions & 0 deletions src/Doctrine/Odm/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\Odm\Filter;

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\ODM\MongoDB\Aggregation\Builder;

final class IriFilter implements FilterInterface, OpenApiParameterFilterInterface, ParameterProviderFilterInterface
{
public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void

Check warning on line 26 in src/Doctrine/Odm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Filter/IriFilter.php#L26

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

Check warning on line 29 in src/Doctrine/Odm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Filter/IriFilter.php#L28-L29

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

\assert($parameter instanceof Parameter);

Check warning on line 32 in src/Doctrine/Odm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Filter/IriFilter.php#L32

Added line #L32 was not covered by tests

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

Check warning on line 36 in src/Doctrine/Odm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Filter/IriFilter.php#L34-L36

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

// TODO: do something for nested properties?
$matchField = $parameter->getProperty();

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

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Filter/IriFilter.php#L40

Added line #L40 was not covered by tests

$aggregationBuilder
->match()
->field($matchField)
->in($value);

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

View check run for this annotation

Codecov / codecov/patch

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

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

public static function getParameterProvider(): string

Check warning on line 48 in src/Doctrine/Odm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Filter/IriFilter.php#L48

Added line #L48 was not covered by tests
{
return IriConverterParameterProvider::class;

Check warning on line 50 in src/Doctrine/Odm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Filter/IriFilter.php#L50

Added line #L50 was not covered by tests
}

public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null

Check warning on line 53 in src/Doctrine/Odm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Filter/IriFilter.php#L53

Added line #L53 was not covered by tests
{
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);

Check warning on line 55 in src/Doctrine/Odm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Filter/IriFilter.php#L55

Added line #L55 was not covered by tests
}

public function getDescription(string $resourceClass): array

Check warning on line 58 in src/Doctrine/Odm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Filter/IriFilter.php#L58

Added line #L58 was not covered by tests
{
return [];

Check warning on line 60 in src/Doctrine/Odm/Filter/IriFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Filter/IriFilter.php#L60

Added line #L60 was not covered by tests
}
}
61 changes: 61 additions & 0 deletions src/Doctrine/Orm/Filter/ExactFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?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;

final class ExactFilter implements FilterInterface, OpenApiParameterFilterInterface, ParameterProviderFilterInterface
{
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
if (!$parameter = $context['parameter'] ?? null) {
return;

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

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Orm/Filter/ExactFilter.php#L30

Added line #L30 was not covered by tests
}

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

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

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

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 [];
}
}
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;

final class IriFilter implements FilterInterface, OpenApiParameterFilterInterface, ParameterProviderFilterInterface
{
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
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#L30

Added line #L30 was 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#L35

Added line #L35 was not covered by tests
}

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

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

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 [];
}
}
4 changes: 2 additions & 2 deletions src/GraphQl/Tests/Type/TypeConverterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type as GraphQLType;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\TypeInfo\Type;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;

/**
* @author Alan Poulain <[email protected]>
Expand Down
2 changes: 1 addition & 1 deletion src/GraphQl/Type/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\TypeIdentifier;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\TypeIdentifier;

/**
* Builds the GraphQL fields.
Expand Down
56 changes: 56 additions & 0 deletions src/State/Provider/IriConverterParameterProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?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\Doctrine\Orm\Filter\IriFilter;
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(
private IriConverterInterface $iriConverter,
) {
}

public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation
{
$operation = $context['operation'] ?? null;
$parameterValue = $parameter->getValue();

$isParameterValueNotSet = !$parameterValue || $parameterValue instanceof ParameterNotFound;
if (!$parameter->getFilter() instanceof IriFilter || $isParameterValueNotSet) {
return $operation;
}

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

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

$parameter->setValue($entities);

return $operation;
}
}
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 = null;

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

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

public function getId(): ?string

Check warning on line 32 in tests/Fixtures/TestBundle/Document/Chicken.php

View check run for this annotation

Codecov / codecov/patch

tests/Fixtures/TestBundle/Document/Chicken.php#L32

Added line #L32 was not covered by tests
{
return $this->id;

Check warning on line 34 in tests/Fixtures/TestBundle/Document/Chicken.php

View check run for this annotation

Codecov / codecov/patch

tests/Fixtures/TestBundle/Document/Chicken.php#L34

Added line #L34 was not covered by tests
}

public function getName(): ?string

Check warning on line 37 in tests/Fixtures/TestBundle/Document/Chicken.php

View check run for this annotation

Codecov / codecov/patch

tests/Fixtures/TestBundle/Document/Chicken.php#L37

Added line #L37 was not covered by tests
{
return $this->name;

Check warning on line 39 in tests/Fixtures/TestBundle/Document/Chicken.php

View check run for this annotation

Codecov / codecov/patch

tests/Fixtures/TestBundle/Document/Chicken.php#L39

Added line #L39 was not covered by tests
}

public function setName(string $name): self

Check warning on line 42 in tests/Fixtures/TestBundle/Document/Chicken.php

View check run for this annotation

Codecov / codecov/patch

tests/Fixtures/TestBundle/Document/Chicken.php#L42

Added line #L42 was not covered by tests
{
$this->name = $name;

Check warning on line 44 in tests/Fixtures/TestBundle/Document/Chicken.php

View check run for this annotation

Codecov / codecov/patch

tests/Fixtures/TestBundle/Document/Chicken.php#L44

Added line #L44 was not covered by tests

return $this;

Check warning on line 46 in tests/Fixtures/TestBundle/Document/Chicken.php

View check run for this annotation

Codecov / codecov/patch

tests/Fixtures/TestBundle/Document/Chicken.php#L46

Added line #L46 was not covered by tests
}

public function getChickenCoop(): ?ChickenCoop

Check warning on line 49 in tests/Fixtures/TestBundle/Document/Chicken.php

View check run for this annotation

Codecov / codecov/patch

tests/Fixtures/TestBundle/Document/Chicken.php#L49

Added line #L49 was not covered by tests
{
return $this->chickenCoop;

Check warning on line 51 in tests/Fixtures/TestBundle/Document/Chicken.php

View check run for this annotation

Codecov / codecov/patch

tests/Fixtures/TestBundle/Document/Chicken.php#L51

Added line #L51 was not covered by tests
}

public function setChickenCoop(?ChickenCoop $chickenCoop): self

Check warning on line 54 in tests/Fixtures/TestBundle/Document/Chicken.php

View check run for this annotation

Codecov / codecov/patch

tests/Fixtures/TestBundle/Document/Chicken.php#L54

Added line #L54 was not covered by tests
{
$this->chickenCoop = $chickenCoop;

Check warning on line 56 in tests/Fixtures/TestBundle/Document/Chicken.php

View check run for this annotation

Codecov / codecov/patch

tests/Fixtures/TestBundle/Document/Chicken.php#L56

Added line #L56 was not covered by tests

return $this;

Check warning on line 58 in tests/Fixtures/TestBundle/Document/Chicken.php

View check run for this annotation

Codecov / codecov/patch

tests/Fixtures/TestBundle/Document/Chicken.php#L58

Added line #L58 was not covered by tests
}
}
Loading
Loading