core icon indicating copy to clipboard operation
core copied to clipboard

SearchFilter exact subresource on another identifier with IRI fails

Open devel-pa opened this issue 5 years ago • 19 comments

API Platform version(s) affected: 2.5.5

Description
SearchFilter subresource with changed identifiers works with identifier but not with IRI identifier

How to reproduce
Add 2 classe like in https://api-platform.com/docs/core/identifiers/#changing-identifier-in-a-doctrine-entity , Person and Process

in class Person add

/**
     * @ORM\ManyToOne(targetEntity="App\Entity\Process", inversedBy="persons")
     */
    private $process;

NOT working

http://localhost/api/person?process=%2Fapi%2Fprocesses%2F65b810c4-9db0-11ea-a2bd-a683e78c6b85

with error: The identifier id is missing for a query of App\\Entity\\Process

Possible Solution
The first request hits vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php line 487 where the entity has no code set as identifier, only the id

Update 1: Ohh my gosh, the SQL is formed around the id of the relation, that's why filtering by UUID only will give an empty array. Must be a filter of the field of subresource `process.uuid'. There is any way to use the IRI for it?

devel-pa avatar May 24 '20 16:05 devel-pa

I am encountering the same issue when filtering on entities that have both numeric ID + uuid fields.

Reproduction

Here's a sandbox project reproducing the bug via the api-platform template with two entities: https://github.com/alexdo/api-platform-uuid-relation Fixtures included; so make sure you run those when trying to reproduce ;)

In the example, a Greeting has many Greeters and Greeters have many Greetings. The uuid-Fields of both entities are marked as @ApiProperty(identifier=true). There's an @ApiFilter(SearchFilter::class, properties={"greeters": "exact"}) annotation on the Greeting class.

I'd expect GET https://localhost:8443/greetings?greeters=[some greeter IRI] to return a collection of greetings present in the many-to-many relation but instead see the same exception as described above: Pastebin link to the full stacktrace

Interestingly, GET https://localhost:8443/greetings?greeters=[some greeter UUID] is working fine. So @ApiProperty(identifier=true) resolution appears to work correctly but IRI resolution + custom identifier doesn't (pure speculation tbh).

Tracking it down

It appears that it all begins in https://github.com/api-platform/core/blob/master/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php#L143 That's calling getIdFromValue() on SearchFilterTrait https://github.com/api-platform/core/blob/master/src/Bridge/Doctrine/Common/Filter/SearchFilterTrait.php#L120-L130 which, in turn, tries to resolve the IRI => ID by hitting the IriConverter->getItemFromIri($value, ['fetch_data' => false]) But, as fetch_data is set to false, the ItemDataProvider will try to create a Reference: https://github.com/api-platform/core/blob/master/src/Bridge/Doctrine/Orm/ItemDataProvider.php#L81-L84 Call with arguments written out:

 $manager->getReference(Greeter::class, ["uuid" => (uuid of my greeter)]);

However, the Doctrine ORM will, of course, only look at the @ORM\Id property - and not at the API one ;)

This isn't a problem when using UUIDs directly, as IriConverter->getItemFromIri just bails out when looking for a matching Route ;)

Fixing this - where to begin?

I'd really like to provide a PR for this but don't really know where to start.

An option could be to prevent resolving the Itemdata in this problematic case (when doctrine id field !== api platform id), just resolve the IRI route attributes and adjust the final query to use uuid instead of the primary key. In other words:

  1. Try to catch or prevent Doctrine\ORM\ORMException: The identifier id is missing for a query of X somehow
  2. Use the raw route "id" attribute instead
  3. => Result: Same behaviour as when just using the UUID (instead of the IRI)

The best way of implementing this could possibly be splitting IriConverter->getItemFromIri into two methods: One that does raw route lookups and another one that uses these lookups + creates the reference.

WDYT?

alexdo avatar Jun 01 '20 22:06 alexdo

I'm facing the same issue at the moment. Watching this issue and willing to give some help ! But it seems to have a huge impact in the core of Api Platform and could potentially introduce huge BC breaks, even if those will apply only when using @ApiProperty.

My current situation: I must have 2 unique identifiers:

  1. one private autoincremented integer generated by the SGBD => id
  2. one UUID to be exposed in every payload/endpoints => publicId

Because I only share the UUID, I added the annotation @ApiProperty on $publicId.

Doing this, all my tests using /entity?relatedEntity=/relatedEntity/<<UUID>> or /entity?relatedEntity=<<UUID>> do fails because the value is mapped to entity.related_entity_id = <<UUID>>.

The ideal situation will be to add a WHERE clause using related_entity.publidId = <<UUID>>.

walva avatar Mar 04 '21 09:03 walva

I'm also facing the same problem, and watching this issue.

I have an Entity with an associated many-to-one relationship to EntityType.

On EntityType I have used the annotation @ApiProperty(identifier=false) on the database ID, and placed annotation @ApiProperty(identifier=true) on a $code property instead.

The result is that EntityType has an IRI like /api/entity_type/ABC instead of /api/entity_type/1 (just an example).

This works correctly for GET requests (eg. GET /api/entity_type/ABC works fine), and also works as expected when performing a POST on Entity, for example using body:

{
  "type": "/api/entity_type/ABC",
  /* other properties here */
}

However, it does not work correctly when using SearchFilter. My annotation is @ApiFilter(SearchFilter::class, properties={"entityType": "exact"}) and I expect to be able to query like this: GET /api/entity?entityType=ABC but this fails, instead I am forced to use the database ID as so: GET /api/entity?entityType=1.

As a workaround, I have changed my SearchFilter annotation to: @ApiFilter(SearchFilter::class, properties={"entityType.code": "exact"}) so now my query looks like this: GET /api/entity?entityType.code=ABC

I tried looking at the code but could not figure out a solution, sorry. Am hoping this gets some attention. Thanks.

pauljura avatar Apr 21 '21 22:04 pauljura

Same here.

For now i'm forcing to use in entityA:

@ApiFilter(SearchFilter::class, properties={"entityB.uuid": "exact"})

naxo8628 avatar May 07 '21 11:05 naxo8628

Based on SearchFilter documentation:

It is possible to filter on relations too, if Offer has a Product relation:

<?php
// api/src/Entity/Offer.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;

#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: ['product' => 'exact'])]
class Offer
{
    // ...
}

With this service definition, it is possible to find all offers belonging to the product identified by a given IRI. Try the following: http://localhost:8000/api/offers?product=/api/products/12. Using a numeric ID is also supported: http://localhost:8000/api/offers?product=12

The above URLs will return all offers for the product having the following IRI as JSON-LD identifier (@id): http://localhost:8000/api/products/12

I also modified the entity identifier, and I can't filter searches by custom IRI id. Even though api-platform correctly identifies the custom relationship of the identifier (like mentioned on documentation) in the entity in any situation using @ApiProperty(identifier=false/true) the problem discussed here occurs.

andrekutianski avatar Jun 05 '21 00:06 andrekutianski

I too am experiencing this same issue, and get "The identifier id is missing for a query of App\\Entity\\Project\\ProjectStage" when filtering on Project using https://example.com/projects?page=1&projectStage=%2Fproject_stages%2Fconstruction . The search filter references an inherited entity, however, it appears that it has nothing to do with inheritance but only using an identifier other than the Doctrine PK id. I tried the possible fixes described by this issue (i.e. search on a sub-property) but when doing so, all records are returned without any filtering. One thought is to rename AbstractRankedList's Doctrine PK from $id to $identifier and API-Platform's identifier from $identifier to $id, but seems like a hack. Anyone have any ideas? Thanks

#[ORM\Entity]
#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: ['projectStage' => 'exact'])]
//#[ApiFilter(SearchFilter::class, properties: ['projectStage.identifier' => 'exact'])]
class Project
{
    #[ORM\ManyToOne(targetEntity: ProjectStage::class)]
    private ?ProjectStage $projectStage = null;

    // Other properties and typical getters and setters
}
#[ORM\Entity]
#[ORM\InheritanceType(value: 'JOINED')]
#[ORM\DiscriminatorColumn(name: 'discriminator', type: 'string')]
#[ORM\UniqueConstraint(columns: ['identifier', 'discriminator'])]
abstract class AbstractRankedList extends AbstractEntity implements RankedListInterface
{
    #[ApiProperty(identifier: false)]
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    #[Ignore]
    protected ?int $id = null;

    #[ApiProperty(identifier: true)]
    #[SerializedName("id")]
    #[ORM\Column(type: 'string', length: 255)]
    protected ?string $identifier = null;

    #[ORM\Column(type: 'string', length: 180)]
    protected ?string $name = null;

    // Other properties and typical getters and setters
}
#[ORM\Entity]
#[ApiResource]
class ProjectStage extends AbstractRankedList
{
}

NotionCommotion avatar Nov 24 '21 14:11 NotionCommotion

Is there any good workaround? I've tried @naxo8628 and @pauljura approach but it doesn't work.

@alexdo how did you solve this issue?

When I do #[ApiFilter(SearchFilter::class, properties: ['user'=>'exact'])] and request ?page=1&user=/api/users/6d4d8ef0-2a8d-416f-b6fb-475d3db0cd0d It actually calls denormalize method from ApiPlatform\Core\Bridge\Symfony\Identifier\Normalizer\UuidNormalizer But then it fails somewhere and gives me that error "The identifier id is missing for a query of App\\Entity\\User". If I request ?page=1&user=6d4d8ef0-2a8d-416f-b6fb-475d3db0cd0d It returns {..."hydra:totalItems": 0,...}.

When I do #[ApiFilter(SearchFilter::class, properties: ['user.uuid'=>'exact'])] and request ?page=1&user.uuid=6d4d8ef0-2a8d-416f-b6fb-475d3db0cd0d It never calls denormalize method from ApiPlatform\Core\Bridge\Symfony\Identifier\Normalizer\UuidNormalizer and then returns:

{
...
"hydra:member": [],
"hydra:totalItems": 0,
...
}

These are my identifier in entity class:

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    #[ApiProperty(['identifier' => false])]
    private ?int $id;

    #[ORM\Column(type: 'uuid', unique: true)]
    #[ApiProperty(['identifier' => true])]
    #[Groups(['user:read', 'user:write', 'filter:read'])]
    private UuidV4 $uuid;

I use use Symfony\Component\Uid\UuidV4; for uuid generation which are stored in db as binary(16).

I tried filtering by fields with string type and it works fine.

nickkadutskyi avatar Feb 11 '22 12:02 nickkadutskyi

Same issue in 2.6.8 experienced today. A fix would be great.

Cruiser13 avatar May 13 '22 15:05 Cruiser13

Same issue here. Issue still exist in 2.7.

dannyvw avatar May 20 '22 10:05 dannyvw

Similar issue here. My related entity has an id field that is named entityId and that seems to throw off the filter with IRI, but the int works fine

Doesn't work: /api/parents?page=1&child=%2Fapi%childs%2F3 "Can't get a way to read the property "id" in class "Proxies\CG\App\Entity\Child"."

Works: /api/parents?page=1&child=3

entityId property in my Child entity is set with #[ApiProperty(['identifier' => true])]

The only workaround I found to use the IRI was to create a getId getter

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

echantigny avatar Jun 03 '22 14:06 echantigny

@soyuka maybe something that could sneak into 2.7? Given that it also exists in 2.7 alpha #4613

Cruiser13 avatar Jun 03 '22 15:06 Cruiser13

I might be completely off here, but I think I found part of the problem. Not sure how to change and test it though.

/api-platform/core/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php Line 127: $associationFieldIdentifier = 'id';

Should that be hardcoded? Thinking that it might need to look at the target class identifier property.

Again, I did not dig really deep into this because there's an "easy" workaround for me.

echantigny avatar Jun 03 '22 15:06 echantigny

There's work that needs to be done on the SearchFilter regarding this issue (hardcoded id and things like this). The work we've done on 2.7-3.0 is meant to be able to fix these issues properly. Still, this is not a priority for now. Please create your own filter, it'll be way easier then trying to work around ours. You can still use the SearchFitler on top of your own IdentityFilter.

soyuka avatar Jun 07 '22 08:06 soyuka

You can still use the SearchFitler on top of your own IdentityFilter.

That's a bit hard since it became final and the Filters are generated services. Can't decorate it

RobinHoutevelts avatar Aug 03 '22 12:08 RobinHoutevelts

Decoration does not have anything to do with a class being final. (Extending is not Decorating!)

You decorate by having a class which gets the decorated class as argument and thus will be an instance of the final class. You implement your logic and pass through anything, non-related to your stuff, into the decorated class instance, or call Methods of the decorated class instance in preferred order and/or change data before or after. Only thing you must assure by decorating is to provide the same interface as the decorated class.

Symfony DI will replace the original service when using decoration with yours and provide the decorated instance as argument to your class https://symfony.com/doc/current/service_container/service_decoration.html

Blackskyliner avatar Aug 19 '22 08:08 Blackskyliner

After reading my previous comment I realize I sounded snarky so let me try again. Keep in mind I'm replying to "You can still use the SearchFilter on top of your own IdentityFilter."

As @Blackskyliner kindly explained this calls for decorating the service (you don't decorate a class but a service).

For example let's take this Resource:

#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: ['eventTravelBlock' => 'exact'])]
#[ORM\Entity(repositoryClass: EventFlightTravelLegRepository::class)]
class EventFlightTravelLeg extends EventTravelLeg
{
    #[ORM\ManyToOne(targetEntity: EventTravelBlock::class)]
    #[ORM\JoinColumn(nullable: false)]
    public EventTravelBlock $eventTravelBlock;
}

#[ApiResource]
#[ORM\Entity(repositoryClass: EventTravelBlockRepository::class)]
class EventTravelBlock
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    #[ApiProperty(identifier: false)]
    public ?int $id = null;

    #[ORM\Column(type: 'uuid')]
    #[ApiProperty(identifier: true)]
    #[SerializedName(serializedName: 'id')]
    public UuidInterface $uuid;
}

Api-Platform generates AnnotationFilterPass.php a annotated_app_entity_event_flight_travel_leg_api_platform_core_bridge_doctrine_orm_filter_search_filter service.

<service id="annotated_app_entity_event_flight_travel_leg_api_platform_core_bridge_doctrine_orm_filter_search_filter" class="ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter" autowire="true">
  <tag name="api_platform.filter"/>
  <argument type="service" id="doctrine"/>
  <argument>null</argument>
  <argument type="service" id="api_platform.iri_converter"/>
  <argument type="service" id="property_accessor"/>
  <argument type="service" id="monolog.logger" on-invalid="ignore"/>
  <argument type="collection">
    <argument key="eventTravelBlock">exact</argument>
  </argument>
  <argument type="service" id="api_platform.identifiers_extractor.cached" on-invalid="ignore"/>
  <argument type="service" id="serializer.name_converter.metadata_aware" on-invalid="ignore"/>
</service>

So let's decorate that:

App\Doctrine\ORM\Filter\SearchFilter:
    decorates: '@annotated_app_entity_event_flight_travel_leg_api_platform_core_bridge_doctrine_orm_filter_search_filter'
<?php

namespace App\Doctrine\ORM\Filter;

use ApiPlatform\Core\Bridge\Doctrine\Common\Filter\SearchFilterInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ContextAwareFilterInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter as BaseSearchFilter;
use Doctrine\ORM\QueryBuilder;

class SearchFilter implements SearchFilterInterface, ContextAwareFilterInterface
{

    public function __construct(
        private readonly BaseSearchFilter $decorated,
    )
    {
    }

    public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = [])
    {
        // TODO: Implement apply() method.
    }

    public function getDescription(string $resourceClass): array
    {
        // TODO: Implement getDescription() method.
    }

}

How do I continue to build "on top" of the existing SearchFilter?

The FilterInterface only has apply and getDescription so I cannot access any of the properties (['eventTravelBlock' => 'exact']) that were given to the decorated SearchFilter.

RobinHoutevelts avatar Aug 19 '22 09:08 RobinHoutevelts

Ontopic: If I remember correctly the issue here is that my identifier is a separate uuid property and the SearchFilter calls IriConverter::getItemFromIri() with ['fetch_data' => 'false'] SearchFilterTrait.php#L123

I added a CompilerPass to replace the IriConverter in the SearchFilters with my own AlwaysFetchingDataIriConverter.


AlwaysFetchingDataIriConverter
<?php

namespace App\Doctrine\Filter;

use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Api\UrlGeneratorInterface;

class AlwaysFetchingDataIriConverter implements IriConverterInterface
{

    public function __construct(
        private readonly IriConverterInterface $iriConverter,
    )
    {
    }

    public function getItemFromIri(string $iri, array $context = [])
    {
        $context['fetch_data'] = true;
        return $this->iriConverter->getItemFromIri($iri, $context);
    }

    public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface::ABS_PATH): string
    {
        return $this->iriConverter->getIriFromItem($item, $referenceType);
    }

    public function getIriFromResourceClass(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): string
    {
        return $this->iriConverter->getIriFromResourceClass($resourceClass, $referenceType);
    }

    public function getItemIriFromResourceClass(string $resourceClass, array $identifiers, int $referenceType = UrlGeneratorInterface::ABS_PATH): string
    {
        return $this->iriConverter->getItemIriFromResourceClass($resourceClass, $identifiers, $referenceType);
    }

    public function getSubresourceIriFromResourceClass(string $resourceClass, array $identifiers, int $referenceType = UrlGeneratorInterface::ABS_PATH): string
    {
        return $this->iriConverter->getSubresourceIriFromResourceClass($resourceClass, $identifiers, $referenceType);
    }

}

class Kernel extends BrefKernel implements CompilerPassInterface {
 
     // ...

    public function process(ContainerBuilder $container): void
    {
        $this->replaceIriConverterInSearchFilter($container);
    }

    /** @see https://github.com/api-platform/core/issues/3575 */
    private function replaceIriConverterInSearchFilter(ContainerBuilder $container): void
    {
        $filterServices = $container->findTaggedServiceIds('api_platform.filter');

        foreach (array_keys($filterServices) as $serviceId) {
            $definition = $container->getDefinition($serviceId);
            if (
                $definition instanceof ChildDefinition
                && is_a($definition->getParent(), SearchFilterInterface::class, true)
            ) {

                // Replace the $iriConverter with one that always fetches data
                $definition->setArgument(
                    '$iriConverter',
                    new Reference(AlwaysFetchingDataIriConverter::class)
                );
            }
        }
    }

RobinHoutevelts avatar Aug 19 '22 09:08 RobinHoutevelts

The Linked FilterPass seems to make use of the DI for class resolution and parameter resolution. So should this not work then:

service.yaml

App\Doctrine\ORM\Filter\SearchFilter:
    decorates: ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter
    arguments:
        $decorated: '@.inner'
namespace App\Doctrine\ORM\Filter;

class SearchFilter implements SearchFilterInterface, ContextAwareFilterInterface
{

    public function __construct(BaseSearchFilter $decorated, ManagerRegistry $managerRegistry, ?RequestStack $requestStack, AlwaysFetchingDataIriConverter $iriConverter, PropertyAccessorInterface $propertyAccessor = null, LoggerInterface $logger = null, array $properties = null, IdentifiersExtractorInterface $identifiersExtractor = null, NameConverterInterface $nameConverter = null)
    {
    }
// ...
}

The decorated should get wired in by services.yaml definition and the other stuff should be handled through autowire or explicit FilterPass injections -- or am I missing why this should not work as intended?

Blackskyliner avatar Aug 19 '22 11:08 Blackskyliner

Above gives:

The definition "App\Doctrine\ORM\Filter\SearchFilter" has a reference to an abstract definition "api_platform.doctrine.orm.search_filter". Abstract definitions cannot be the target of references.

And

App\Doctrine\ORM\Filter\SearchFilter:
    decorates: 'annotated_app_entity_event_flight_travel_leg_api_platform_core_bridge_doctrine_orm_filter_search_filter'
    arguments:
        $decorated: '@.inner'

Doesn't autowire the $properties argument.

I could add it myself but I would like to avoid repeating myself and/or adding a decorator each time I want to use the SearchFilter.

RobinHoutevelts avatar Aug 19 '22 15:08 RobinHoutevelts

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Nov 04 '22 21:11 stale[bot]

I might be completely off here, but can we unblock the situation by updating ApiPlatform\Doctrine\Common\Filter\SearchFilterTrait at line 123 ? (before rework on SearchFilter as @soyuka said)

protected function getIdFromValue(string $value)
    {
        try {
            $iriConverter = $this->getIriConverter();
            $item = $iriConverter->getResourceFromIri($value, ['fetch_data' => true]); //$item = $iriConverter->getResourceFromIri($value, ['fetch_data' => false]);

            return $this->getPropertyAccessor()->getValue($item, 'id');
        } catch (InvalidArgumentException $e) {
            // Do nothing, return the raw value
        }

        return $value;
    }

It's working on my side by just overriding this method of the SearchFilterTrait in my own SearchFilter.

nathan-de-pachtere avatar Nov 26 '22 17:11 nathan-de-pachtere

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Jan 25 '23 19:01 stale[bot]

On API Platform 2.5 I did this way. It can certainly be improved

<?php
/*
 * @ApiFilter(UuidSearchFilter::class, properties={"author"})
 */
class Books
{
    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Author", inversedBy="books")
     */
    protected $author;

The filter


<?php

namespace App\Api\Filter;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Exception\ItemNotFoundException;
use Doctrine\ORM\QueryBuilder;

final class UuidSearchFilter extends SearchFilter
{
    protected function filterProperty(
        string $property,
        $value,
        QueryBuilder $queryBuilder,
        QueryNameGeneratorInterface $queryNameGenerator,
        string $resourceClass,
        string $operationName = null
    ) {
        $items = [];
        $values = (is_array($value)) ? $value : (array) $value;
        foreach ($values as $iri) {
            try {
                // Get entity from IRI
                $items[] = $this->iriConverter->getItemFromIri($iri);
            } catch (ItemNotFoundException | InvalidArgumentException $e) {
            }
        }

        $rootAlias = $queryBuilder->getRootAliases()[0];
        $parameterName = $queryNameGenerator->generateParameterName($property);
        $joinAlias = $queryNameGenerator->generateJoinAlias($property);
        // Join the related entity
        $queryBuilder->join(sprintf('%s.%s', $rootAlias, $property), $joinAlias);
        $queryBuilder
            ->andWhere($queryBuilder->expr()->in(sprintf('%s.%s', $rootAlias, $property), ':'.$parameterName))
            ->setParameter($parameterName, $items)
        ;
    }
}

orangevinz avatar May 31 '23 17:05 orangevinz

If you follow the doc:

<?php
// api/src/Entity/Offer.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;

#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: ['product' => 'exact'])]
class Offer
{
    // ...
}

And you don't use Bridge namespace ApiPlatform\Core\Bridge\Doctrine\... like @orangevinz did. His reply will not work.

Instead, use @nathan-de-pachtere answer as follows:

<?php

namespace App\Doctrine\Filter;

use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;

final class UuidSearchFilter extends SearchFilter
{
    protected function getIdFromValue(string $value)
    {
        try {
            $iriConverter = $this->getIriConverter();
            $item         = $iriConverter->getResourceFromIri($value, ['fetch_data' => true]);

            return $this->getPropertyAccessor()->getValue($item, 'id');
        } catch (InvalidArgumentException $e) {
            // Do nothing, return the raw value
        }

        return $value;
    }
}

And use it like this:

<?php
// api/src/Entity/Offer.php
namespace App\Entity;

+ use App\Doctrine\Filter\UuidSearchFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\ApiFilter;
- use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;

#[ApiResource]
- #[ApiFilter(SearchFilter::class, properties: ['product' => 'exact'])]
+ #[ApiFilter(UuidSearchFilter::class, properties: ['product'])]
class Offer
{
    // ...
}

cavasinf avatar Feb 12 '24 17:02 cavasinf