Skip to content

feat(doctrine): searchfilter using id instead of full IRI when it's not an int #5771

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
13 changes: 12 additions & 1 deletion features/doctrine/search_filter.feature
Original file line number Diff line number Diff line change
Expand Up @@ -1045,10 +1045,21 @@ Feature: Search filter on collections

@!mongodb
@createSchema
Scenario: Filters can use UUIDs
Scenario: Filters can use UUID based IRIs
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"
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

@!mongodb
@createSchema
Scenario: Filters can use UUID directly
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[]=61817181-0ecc-42fb-a6e7-d97f2ddcb344&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

48 changes: 40 additions & 8 deletions src/Doctrine/Orm/Filter/SearchFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
use ApiPlatform\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\IriConverterInterface;
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;
Expand Down Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

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

lots of new dependencies I wish we wouldn't have...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, that's mostly why i didn't include this part in the previous PR. And that's kind of the point I was making in #5687 - some of the metadata is not easy to get to at times.

}

protected function getIriConverter(): IriConverterInterface
Expand Down Expand Up @@ -221,9 +236,22 @@ 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);
$variables = $associationApiMetadata->getOperation()->getUriVariables();
if (1 === \count($variables)) { // otherwise let's just give up at this point, too complicated
$varName = array_key_first($variables);
$associationResourceApiIdField = $variables[$varName]->getIdentifiers()[0]; // this will be needed to get the correct value
Copy link
Member

Choose a reason for hiding this comment

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

agreed this is too complicated and deserves its own search filter...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, with multiple identifiers this becomes even clumsier to handle, and i'm not even sure what the request should look like anyway.

Copy link
Member

Choose a reason for hiding this comment

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

Best would be to make some mix with #5732 to handle complicated stuff not sure how it'd apply on filters though

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A similar handlelinks option in filters?

That's a really interesting idea, but every filter would need to be reworked, that's quite an impact. I'd keep that idea in mind with #2400 for a major overhaul of filters, maybe?

}
}

$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;
}
Expand All @@ -232,11 +260,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)),
Expand Down