Skip to content
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

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

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 12 additions & 1 deletion features/doctrine/search_filter.feature
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
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 @@
$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();

Check failure on line 244 in src/Doctrine/Orm/Filter/SearchFilter.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.2)

Call to an undefined method ApiPlatform\Metadata\Operation::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

Check warning on line 247 in src/Doctrine/Orm/Filter/SearchFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Orm/Filter/SearchFilter.php#L243-L247

Added lines #L243 - L247 were not covered by tests
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 @@

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);

Check warning on line 269 in src/Doctrine/Orm/Filter/SearchFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Orm/Filter/SearchFilter.php#L266-L269

Added lines #L266 - L269 were not covered by tests
}

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