Skip to content

Commit

Permalink
feat: simplification on ORM SearchFilter for associations
Browse files Browse the repository at this point in the history
  • Loading branch information
mrossard committed Aug 10, 2023
1 parent ebf0310 commit 57841a8
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 20 deletions.
9 changes: 9 additions & 0 deletions features/doctrine/search_filter.feature
Original file line number Diff line number Diff line change
Expand Up @@ -1042,3 +1042,12 @@ Feature: Search filter on collections
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 1

@!mongodb
@createSchema
Scenario: Filters can use UUIDs
Given there is a group object with uuid "61817181-0ecc-42fb-a6e7-d97f2ddcb344" and 2 users
When I send a "GET" request to "/issue5735/users?groups=/issue5735/groups/61817181-0ecc-42fb-a6e7-d97f2ddcb344"
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 2
52 changes: 33 additions & 19 deletions src/Doctrine/Orm/Filter/SearchFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use ApiPlatform\Api\IriConverterInterface;
use ApiPlatform\Doctrine\Common\Filter\SearchFilterInterface;
use ApiPlatform\Doctrine\Common\Filter\SearchFilterTrait;
use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\Operation;
Expand Down Expand Up @@ -217,30 +216,45 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
return;
}

$values = array_map($this->getIdFromValue(...), $values);
// todo: handle composite IDs
/**
* For an association, just get the associated entity (or a reference) and add a simple join!
*/
$rootAlias = $alias;
$joinAlias = $queryNameGenerator->generateJoinAlias($field);
$joinParameter = $queryNameGenerator->generateParameterName($field);

$associationResourceClass = $metadata->getAssociationTargetClass($field);
$associationMetadata = $this->getClassMetadata($associationResourceClass);
$associationFieldIdentifier = $associationMetadata->getIdentifierFieldNames()[0];
$doctrineTypeField = $this->getDoctrineFieldType($associationFieldIdentifier, $associationResourceClass);
try {
$item = $this->getIriConverter()->getResourceFromIri($value, ['fetch_data' => false]);

if (!$this->hasValidValues($values, $doctrineTypeField)) {
$this->logger->notice('Invalid filter ignored', [
'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $field)),
]);

return;
if($metadata->isCollectionValuedAssociation($field)){
$joinCondition = sprintf(':%s MEMBER OF %s.%s', $joinParameter, $rootAlias, $field);
}
else{
$joinCondition = sprintf(':%s = %s.%s', $joinParameter, $rootAlias, $field);
}
}
catch (InvalidArgumentException) {
//not an IRI, get the identifier the old way
$associationResourceClass = $metadata->getAssociationTargetClass($field);
$associationMetadata = $this->getClassMetadata($associationResourceClass);
$associationFieldIdentifier = $associationMetadata->getIdentifierFieldNames()[0];
$doctrineTypeField = $this->getDoctrineFieldType($associationFieldIdentifier, $associationResourceClass);

if (!$this->hasValidValues($values, $doctrineTypeField)) {
$this->logger->notice('Invalid filter ignored', [
'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $field)),
]);

$associationAlias = $alias;
$associationField = $field;
if ($metadata->isCollectionValuedAssociation($associationField) || $metadata->isAssociationInverseSide($field)) {
$associationAlias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, $alias, $associationField);
$associationField = $associationFieldIdentifier;
return;
}

$joinCondition = sprintf('%s.%s = :%s', $joinAlias, $associationFieldIdentifier, $joinParameter);
$item = $value;
}

$this->addWhereByStrategy($strategy, $queryBuilder, $queryNameGenerator, $associationAlias, $associationField, $values, $caseSensitive);

$queryBuilder->join(sprintf('%s.%s', $rootAlias, $field), $joinAlias, Join::WITH , $joinCondition)
->setParameter($joinParameter, $item);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/Doctrine/Orm/State/ItemProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
$manager = $this->managerRegistry->getManagerForClass($entityClass);

$fetchData = $context['fetch_data'] ?? true;
if (!$fetchData) {
if (!$fetchData && \array_key_exists('id', $uriVariables)) {
// todo : if uriVariables don't contain the id, this fails. This should behave like it does in the following code
return $manager->getReference($entityClass, $uriVariables);
}

Expand Down
24 changes: 24 additions & 0 deletions tests/Behat/DoctrineContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\InitializeInput;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\InternalUser;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\IriOnlyDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5735\Group;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MaxDepthDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsRelatedDummy;
Expand Down Expand Up @@ -2159,6 +2160,29 @@ public function thereIsADummyWithSubEntity(string $strId, string $name): void
$this->manager->flush();
}

/**
* @Given there is a group object with uuid :uuid and :nbUsers users
*/
public function thereIsAGroupWithUuidAndNUsers(string $uuid, int $nbUsers): void
{
$group = new Group();
$group->setUuid(\Symfony\Component\Uid\Uuid::fromString($uuid));

$this->manager->persist($group);

for ($i = 0; $i < $nbUsers; ++$i) {
$user = new \ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5735\User();
$user->addGroup($group);
$this->manager->persist($user);
}

// add another user not in this group
$user = new \ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5735\User();
$this->manager->persist($user);

$this->manager->flush();
}

private function isOrm(): bool
{
return null !== $this->schemaTool;
Expand Down
75 changes: 75 additions & 0 deletions tests/Fixtures/TestBundle/Entity/Issue5735/Group.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\Entity\Issue5735;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;

#[ApiResource(
operations : [new Get(), new GetCollection()],
routePrefix: '/issue5735'
)]
#[ORM\Entity]
#[ORM\Table(name: 'issue5735_group')]
class Group
{
#[ApiProperty(readable: false, writable: false, identifier: false)]
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private int $id;

#[ApiProperty(writable: false, identifier: true)]
#[ORM\Column(type: 'symfony_uuid', unique: true)]
private Uuid $uuid;

#[ORM\ManyToMany(targetEntity: User::class, inversedBy: 'groups')]
private Collection $users;

public function __construct()
{
$this->users = new ArrayCollection();
}

public function getUuid(): Uuid
{
return $this->uuid;
}

public function setUuid(Uuid $uuid): void
{
$this->uuid = $uuid;
}

public function getUsers(): Collection
{
return $this->users;
}

public function addUser(User $user): void
{
$this->users->add($user);
}

public function getId(): int
{
return $this->id;
}
}
85 changes: 85 additions & 0 deletions tests/Fixtures/TestBundle/Entity/Issue5735/User.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\Entity\Issue5735;

use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;

#[ApiResource(
operations: [
new Get(),
new GetCollection(),
],
routePrefix: '/issue5735'
)]
#[ApiFilter(SearchFilter::class, properties: ['groups' => 'exact'])]
#[ORM\Entity]
#[ORM\Table(name: 'issue5735_user')]
class User
{
#[ApiProperty(readable: false, writable: false, identifier: false)]
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private int $id;

#[ApiProperty(writable: false, identifier: true)]
#[ORM\Column(type: 'symfony_uuid', unique: true)]
private Uuid $uuid;

#[ORM\ManyToMany(targetEntity: Group::class, mappedBy: 'users')]
private Collection $groups;

public function __construct()
{
$this->groups = new ArrayCollection();
$this->uuid = Uuid::v4();
}

public function getUuid(): Uuid
{
return $this->uuid;
}

public function setUuid($uuid): void
{
$this->uuid = $uuid;
}

public function getGroups(): Collection
{
return $this->groups;
}

public function addGroup(Group $group): void
{
$this->groups->add($group);
if (!$group->getUsers()->contains($this)) {
$group->addUser($this);
}
}

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

0 comments on commit 57841a8

Please sign in to comment.