From 6cd4adf20ae876ccf08942f73f21a9607c9b73b3 Mon Sep 17 00:00:00 2001 From: Manuel Rossard Date: Wed, 23 Aug 2023 17:55:45 +0200 Subject: [PATCH] feat(doctrine): searchfilter using id instead of full IRI when it's not an int --- features/doctrine/search_filter.feature | 2 +- src/Doctrine/Orm/Filter/SearchFilter.php | 51 ++++++++++++++++++++---- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/features/doctrine/search_filter.feature b/features/doctrine/search_filter.feature index 119a64a8b61..b8fe64014d4 100644 --- a/features/doctrine/search_filter.feature +++ b/features/doctrine/search_filter.feature @@ -1048,7 +1048,7 @@ Feature: Search filter on collections Scenario: Filters can use UUIDs Given there is a group object with uuid "61817181-0ecc-42fb-a6e7-d97f2ddcb344" and 2 users And there is a group object with uuid "32510d53-f737-4e70-8d9d-58e292c871f8" and 1 users - When I send a "GET" request to "/issue5735/issue5735_users?groups[]=/issue5735/groups/61817181-0ecc-42fb-a6e7-d97f2ddcb344&groups[]=/issue5735/groups/32510d53-f737-4e70-8d9d-58e292c871f8" + When I send a "GET" request to "/issue5735/issue5735_users?groups[]=61817181-0ecc-42fb-a6e7-d97f2ddcb344&groups[]=/issue5735/groups/32510d53-f737-4e70-8d9d-58e292c871f8" Then the response status code should be 200 And the response should be in JSON And the JSON node "hydra:totalItems" should be equal to 3 diff --git a/src/Doctrine/Orm/Filter/SearchFilter.php b/src/Doctrine/Orm/Filter/SearchFilter.php index af1f9cb92ad..c49a33d5b8c 100644 --- a/src/Doctrine/Orm/Filter/SearchFilter.php +++ b/src/Doctrine/Orm/Filter/SearchFilter.php @@ -21,6 +21,9 @@ use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; @@ -133,17 +136,29 @@ */ final class SearchFilter extends AbstractFilter implements SearchFilterInterface { + use ResourceClassInfoTrait; use SearchFilterTrait; public const DOCTRINE_INTEGER_TYPE = Types::INTEGER; - public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, PropertyAccessorInterface $propertyAccessor = null, LoggerInterface $logger = null, array $properties = null, IdentifiersExtractorInterface $identifiersExtractor = null, NameConverterInterface $nameConverter = null) - { + public function __construct( + ManagerRegistry $managerRegistry, + IriConverterInterface $iriConverter, + PropertyAccessorInterface $propertyAccessor = null, + LoggerInterface $logger = null, + array $properties = null, + IdentifiersExtractorInterface $identifiersExtractor = null, + NameConverterInterface $nameConverter = null, + ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, + ResourceClassResolverInterface $resourceClassResolver = null + ) { parent::__construct($managerRegistry, $logger, $properties, $nameConverter); $this->iriConverter = $iriConverter; $this->identifiersExtractor = $identifiersExtractor; $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->resourceClassResolver = $resourceClassResolver; } protected function getIriConverter(): IriConverterInterface @@ -221,9 +236,25 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB $associationResourceClass = $metadata->getAssociationTargetClass($field); $associationMetadata = $this->getClassMetadata($associationResourceClass); $associationFieldIdentifier = $associationMetadata->getIdentifierFieldNames()[0]; + $associationResourceApiIdField = $associationFieldIdentifier; + + // dig into the subResource metadata to find out which field we're looking at + if ($this->isResourceClass($associationResourceClass) && $this->resourceMetadataFactory) { + $associationApiMetadata = $this->resourceMetadataFactory->create($associationResourceClass); + $it = $associationApiMetadata->getIterator(); + $current = $it->current(); + $variables = $current->getUriVariables(); + if (1 === \count($variables)) { // otherwise let's just give up at this point, too complicated + $varName = array_key_first($variables); + $link = $variables[$varName]; + $associationResourceApiIdField = $link->getIdentifiers()[0]; // this will be needed + } + } + $doctrineTypeField = $this->getDoctrineFieldType($associationFieldIdentifier, $associationResourceClass); + $associationRepository = $this->managerRegistry->getManagerForClass($associationResourceClass)?->getRepository($associationResourceClass); - $values = array_map(function ($value) use ($associationFieldIdentifier, $doctrineTypeField) { + $values = array_map(function ($value) use ($associationFieldIdentifier, $associationResourceApiIdField, $doctrineTypeField, $associationRepository) { if (is_numeric($value)) { return $value; } @@ -232,11 +263,15 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB return $this->propertyAccessor->getValue($item, $associationFieldIdentifier); } catch (InvalidArgumentException) { - /* - * Can we do better? This is not the ApiResource the call was made on, - * so we don't get any kind of api metadata for it without (a lot of?) work elsewhere... - * Let's just pretend it's always the ORM id for now. - */ + // replace the value of $associationResourceApiIdField by that of $associationFieldIdentifier if necessary! + if ($associationFieldIdentifier !== $associationResourceApiIdField && $associationRepository) { + // use $associationResourceApiIdField to fetch the entity + $entity = $associationRepository->findOneBy([ + $associationResourceApiIdField => $value, + ]); + $value = $this->getPropertyAccessor()->getValue($entity, $associationFieldIdentifier); + } + if (!$this->hasValidValues([$value], $doctrineTypeField)) { $this->logger->notice('Invalid filter ignored', [ 'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $associationFieldIdentifier)),