diff --git a/src/Doctrine/Odm/Filter/ExactFilter.php b/src/Doctrine/Odm/Filter/ExactFilter.php
new file mode 100644
index 00000000000..13ab138684f
--- /dev/null
+++ b/src/Doctrine/Odm/Filter/ExactFilter.php
@@ -0,0 +1,56 @@
+
+ *
+ * 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\Doctrine\Odm\Filter;
+
+use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Metadata\Parameter;
+use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
+use Doctrine\ODM\MongoDB\Aggregation\Builder;
+
+final class ExactFilter implements FilterInterface, OpenApiParameterFilterInterface
+{
+ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
+ {
+ if (!$parameter = $context['parameter'] ?? null) {
+ return;
+ }
+
+ \assert($parameter instanceof Parameter);
+
+ $values = (array) $parameter->getValue();
+ if ([] === $values) {
+ return;
+ }
+
+ $matchField = $parameter->getProperty();
+ $fieldQuery = $aggregationBuilder->match()->field($matchField);
+
+ if (\count($values) > 1) {
+ $fieldQuery->in($values);
+ } else {
+ $fieldQuery->equals(reset($values));
+ }
+ }
+
+ public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
+ {
+ return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
+ }
+
+ public function getDescription(string $resourceClass): array
+ {
+ return [];
+ }
+}
diff --git a/src/Doctrine/Odm/Filter/IriFilter.php b/src/Doctrine/Odm/Filter/IriFilter.php
new file mode 100644
index 00000000000..67d8f2be221
--- /dev/null
+++ b/src/Doctrine/Odm/Filter/IriFilter.php
@@ -0,0 +1,61 @@
+
+ *
+ * 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\Doctrine\Odm\Filter;
+
+use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Metadata\Parameter;
+use ApiPlatform\Metadata\ParameterProviderFilterInterface;
+use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
+use ApiPlatform\State\Provider\IriConverterParameterProvider;
+use Doctrine\ODM\MongoDB\Aggregation\Builder;
+
+final class IriFilter implements FilterInterface, OpenApiParameterFilterInterface, ParameterProviderFilterInterface
+{
+ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
+ {
+ if (!$parameter = $context['parameter'] ?? null) {
+ return;
+ }
+
+ \assert($parameter instanceof Parameter);
+
+ $value = $parameter->getValue();
+ if (!\is_array($value)) {
+ $value = [$value];
+ }
+
+ $matchField = $parameter->getProperty();
+
+ $aggregationBuilder
+ ->match()
+ ->field($matchField)
+ ->in($value);
+ }
+
+ public static function getParameterProvider(): string
+ {
+ return IriConverterParameterProvider::class;
+ }
+
+ public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
+ {
+ return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
+ }
+
+ public function getDescription(string $resourceClass): array
+ {
+ return [];
+ }
+}
diff --git a/src/Doctrine/Odm/Filter/PartialSearchFilter.php b/src/Doctrine/Odm/Filter/PartialSearchFilter.php
new file mode 100644
index 00000000000..2f08198211f
--- /dev/null
+++ b/src/Doctrine/Odm/Filter/PartialSearchFilter.php
@@ -0,0 +1,56 @@
+
+ *
+ * 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\Doctrine\Odm\Filter;
+
+use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Metadata\Parameter;
+use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
+use Doctrine\ODM\MongoDB\Aggregation\Builder;
+use MongoDB\BSON\Regex;
+
+final class PartialSearchFilter implements FilterInterface, OpenApiParameterFilterInterface
+{
+ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
+ {
+ if (!$parameter = $context['parameter'] ?? null) {
+ return;
+ }
+
+ \assert($parameter instanceof Parameter);
+
+ $value = $parameter->getValue();
+ if (!\is_string($value) || '' === $value) {
+ return;
+ }
+
+ $matchField = $parameter->getProperty();
+ $escapedValue = preg_quote($value, '/');
+
+ $aggregationBuilder
+ ->match()
+ ->field($matchField)
+ ->equals(new Regex($escapedValue, 'i'));
+ }
+
+ public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
+ {
+ return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
+ }
+
+ public function getDescription(string $resourceClass): array
+ {
+ return [];
+ }
+}
diff --git a/src/Doctrine/Orm/Filter/ExactFilter.php b/src/Doctrine/Orm/Filter/ExactFilter.php
new file mode 100644
index 00000000000..89e4c368381
--- /dev/null
+++ b/src/Doctrine/Orm/Filter/ExactFilter.php
@@ -0,0 +1,54 @@
+
+ *
+ * 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\Doctrine\Orm\Filter;
+
+use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Metadata\Parameter;
+use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
+use Doctrine\ORM\QueryBuilder;
+
+final class ExactFilter implements FilterInterface, OpenApiParameterFilterInterface
+{
+ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
+ {
+ if (!$parameter = $context['parameter'] ?? null) {
+ return;
+ }
+
+ $value = $parameter->getValue();
+ if (!\is_array($value)) {
+ $value = [$value];
+ }
+
+ $property = $parameter->getProperty();
+ $alias = $queryBuilder->getRootAliases()[0];
+ $parameterName = $queryNameGenerator->generateParameterName($property);
+
+ $queryBuilder
+ ->andWhere(\sprintf('%s.%s = :%s', $alias, $property, $parameterName))
+ ->setParameter($parameterName, $value);
+ }
+
+ public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
+ {
+ return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
+ }
+
+ public function getDescription(string $resourceClass): array
+ {
+ return [];
+ }
+}
diff --git a/src/Doctrine/Orm/Filter/IriFilter.php b/src/Doctrine/Orm/Filter/IriFilter.php
new file mode 100644
index 00000000000..0dd128c9239
--- /dev/null
+++ b/src/Doctrine/Orm/Filter/IriFilter.php
@@ -0,0 +1,62 @@
+
+ *
+ * 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\Doctrine\Orm\Filter;
+
+use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Metadata\Parameter;
+use ApiPlatform\Metadata\ParameterProviderFilterInterface;
+use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
+use ApiPlatform\State\Provider\IriConverterParameterProvider;
+use Doctrine\ORM\QueryBuilder;
+
+final class IriFilter implements FilterInterface, OpenApiParameterFilterInterface, ParameterProviderFilterInterface
+{
+ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
+ {
+ if (!$parameter = $context['parameter'] ?? null) {
+ return;
+ }
+
+ $value = $parameter->getValue();
+ if (!\is_array($value)) {
+ $value = [$value];
+ }
+
+ $property = $parameter->getProperty();
+ $alias = $queryBuilder->getRootAliases()[0];
+ $parameterName = $queryNameGenerator->generateParameterName($property);
+
+ $queryBuilder
+ ->join(\sprintf('%s.%s', $alias, $property), $parameterName)
+ ->andWhere(\sprintf('%s IN(:%s)', $parameterName, $parameterName))
+ ->setParameter($parameterName, $value);
+ }
+
+ public static function getParameterProvider(): string
+ {
+ return IriConverterParameterProvider::class;
+ }
+
+ public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
+ {
+ return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
+ }
+
+ public function getDescription(string $resourceClass): array
+ {
+ return [];
+ }
+}
diff --git a/src/Doctrine/Orm/Filter/PartialSearchFilter.php b/src/Doctrine/Orm/Filter/PartialSearchFilter.php
new file mode 100644
index 00000000000..948d0835ea4
--- /dev/null
+++ b/src/Doctrine/Orm/Filter/PartialSearchFilter.php
@@ -0,0 +1,58 @@
+
+ *
+ * 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\Doctrine\Orm\Filter;
+
+use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Metadata\Parameter;
+use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
+use Doctrine\ORM\QueryBuilder;
+
+final class PartialSearchFilter implements FilterInterface, OpenApiParameterFilterInterface
+{
+ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
+ {
+ if (!$parameter = $context['parameter'] ?? null) {
+ return;
+ }
+
+ $value = $parameter->getValue();
+
+ $property = $parameter->getProperty();
+ $alias = $queryBuilder->getRootAliases()[0];
+ $field = $alias.'.'.$property;
+
+ $parameterName = $queryNameGenerator->generateParameterName($property);
+
+ $likeExpression = $queryBuilder->expr()->like(
+ 'LOWER('.$field.')',
+ ':'.$parameterName
+ );
+
+ $queryBuilder
+ ->andWhere($likeExpression)
+ ->setParameter($parameterName, '%'.strtolower($value).'%');
+ }
+
+ public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
+ {
+ return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
+ }
+
+ public function getDescription(string $resourceClass): array
+ {
+ return [];
+ }
+}
diff --git a/src/State/Provider/IriConverterParameterProvider.php b/src/State/Provider/IriConverterParameterProvider.php
new file mode 100644
index 00000000000..41509b54ad6
--- /dev/null
+++ b/src/State/Provider/IriConverterParameterProvider.php
@@ -0,0 +1,56 @@
+
+ *
+ * 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\State\Provider;
+
+use ApiPlatform\Doctrine\Orm\Filter\IriFilter;
+use ApiPlatform\Metadata\IriConverterInterface;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Metadata\Parameter;
+use ApiPlatform\State\ParameterNotFound;
+use ApiPlatform\State\ParameterProviderInterface;
+
+/**
+ * @author Vincent Amstoutz
+ */
+final readonly class IriConverterParameterProvider implements ParameterProviderInterface
+{
+ public function __construct(
+ private IriConverterInterface $iriConverter,
+ ) {
+ }
+
+ public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation
+ {
+ $operation = $context['operation'] ?? null;
+ $parameterValue = $parameter->getValue();
+
+ $isParameterValueNotSet = !$parameterValue || $parameterValue instanceof ParameterNotFound;
+ if (!$parameter->getFilter() instanceof IriFilter || $isParameterValueNotSet) {
+ return $operation;
+ }
+
+ if (!\is_array($parameterValue)) {
+ $parameterValue = [$parameterValue];
+ }
+
+ $entities = [];
+ foreach ($parameterValue as $iri) {
+ $entities[] = $this->iriConverter->getResourceFromIri($iri, ['fetch_data' => false]);
+ }
+
+ $parameter->setValue($entities);
+
+ return $operation;
+ }
+}
diff --git a/src/Symfony/Bundle/Resources/config/state/provider.xml b/src/Symfony/Bundle/Resources/config/state/provider.xml
index 52bb2ea23f7..45f19b9eb9d 100644
--- a/src/Symfony/Bundle/Resources/config/state/provider.xml
+++ b/src/Symfony/Bundle/Resources/config/state/provider.xml
@@ -42,5 +42,11 @@
+
+
+
+
+
+
diff --git a/tests/Fixtures/TestBundle/Document/Chicken.php b/tests/Fixtures/TestBundle/Document/Chicken.php
new file mode 100644
index 00000000000..e556219d098
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Document/Chicken.php
@@ -0,0 +1,72 @@
+
+ *
+ * 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\Document;
+
+use ApiPlatform\Doctrine\Odm\Filter\ExactFilter;
+use ApiPlatform\Doctrine\Odm\Filter\PartialSearchFilter;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\QueryParameter;
+use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
+
+#[ODM\Document]
+#[GetCollection(
+ parameters: [
+ 'chickenCoop' => new QueryParameter(filter: new ExactFilter()),
+ 'name' => new QueryParameter(filter: new ExactFilter()),
+ 'namePartial' => new QueryParameter(
+ filter: new PartialSearchFilter(),
+ property: 'name',
+ ),
+ ],
+)]
+class Chicken
+{
+ #[ODM\Id]
+ private ?string $id = null;
+
+ #[ODM\Field(type: 'string')]
+ private string $name;
+
+ #[ODM\ReferenceOne(targetDocument: ChickenCoop::class, inversedBy: 'chickens')]
+ private ?ChickenCoop $chickenCoop = null;
+
+ public function getId(): ?string
+ {
+ return $this->id;
+ }
+
+ public function getName(): ?string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): self
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ public function getChickenCoop(): ?ChickenCoop
+ {
+ return $this->chickenCoop;
+ }
+
+ public function setChickenCoop(?ChickenCoop $chickenCoop): self
+ {
+ $this->chickenCoop = $chickenCoop;
+
+ return $this;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Document/ChickenCoop.php b/tests/Fixtures/TestBundle/Document/ChickenCoop.php
new file mode 100644
index 00000000000..50e925b8314
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Document/ChickenCoop.php
@@ -0,0 +1,74 @@
+
+ *
+ * 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\Document;
+
+use ApiPlatform\Doctrine\Odm\Filter\IriFilter;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\QueryParameter;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
+
+#[ODM\Document]
+#[GetCollection(
+ normalizationContext: ['hydra_prefix' => false],
+ parameters: ['chickens' => new QueryParameter(filter: new IriFilter())])
+]
+class ChickenCoop
+{
+ #[ODM\Id]
+ private ?string $id = null;
+
+ #[ODM\ReferenceMany(targetDocument: Chicken::class, mappedBy: 'chickenCoop')]
+ private Collection $chickens;
+
+ public function __construct()
+ {
+ $this->chickens = new ArrayCollection();
+ }
+
+ public function getId(): ?string
+ {
+ return $this->id;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getChickens(): Collection
+ {
+ return $this->chickens;
+ }
+
+ public function addChicken(Chicken $chicken): self
+ {
+ if (!$this->chickens->contains($chicken)) {
+ $this->chickens[] = $chicken;
+ $chicken->setChickenCoop($this);
+ }
+
+ return $this;
+ }
+
+ public function removeChicken(Chicken $chicken): self
+ {
+ if ($this->chickens->removeElement($chicken)) {
+ if ($chicken->getChickenCoop() === $this) {
+ $chicken->setChickenCoop(null);
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Document/Company.php b/tests/Fixtures/TestBundle/Document/Company.php
index ca88faa6e44..6b3ab63fc03 100644
--- a/tests/Fixtures/TestBundle/Document/Company.php
+++ b/tests/Fixtures/TestBundle/Document/Company.php
@@ -25,13 +25,19 @@
#[GetCollection]
#[Get]
#[Post]
-#[ApiResource(uriTemplate: '/employees/{employeeId}/rooms/{roomId}/company/{companyId}', uriVariables: ['employeeId' => ['from_class' => Employee::class, 'from_property' => 'company']])]
+#[ApiResource(
+ uriTemplate: '/employees/{employeeId}/rooms/{roomId}/company/{companyId}',
+ uriVariables: ['employeeId' => ['from_class' => Employee::class, 'from_property' => 'company']]
+)]
#[Get]
-#[ApiResource(uriTemplate: '/employees/{employeeId}/company', uriVariables: ['employeeId' => ['from_class' => Employee::class, 'from_property' => 'company']])]
+#[ApiResource(
+ uriTemplate: '/employees/{employeeId}/company',
+ uriVariables: ['employeeId' => ['from_class' => Employee::class, 'from_property' => 'company']]
+)]
#[ODM\Document]
class Company
{
- #[ODM\Id(strategy: 'INCREMENT', type: 'int')]
+ #[ODM\Id(type: 'int', strategy: 'INCREMENT')]
private ?int $id = null;
#[ODM\Field]
diff --git a/tests/Fixtures/TestBundle/Entity/Chicken.php b/tests/Fixtures/TestBundle/Entity/Chicken.php
new file mode 100644
index 00000000000..9d497676608
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Entity/Chicken.php
@@ -0,0 +1,75 @@
+
+ *
+ * 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;
+
+use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
+use ApiPlatform\Doctrine\Orm\Filter\PartialSearchFilter;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\QueryParameter;
+use Doctrine\ORM\Mapping as ORM;
+
+#[ORM\Entity]
+#[GetCollection(
+ parameters: [
+ 'chickenCoop' => new QueryParameter(filter: new ExactFilter()),
+ 'name' => new QueryParameter(filter: new ExactFilter()),
+ 'namePartial' => new QueryParameter(
+ filter: new PartialSearchFilter(),
+ property: 'name',
+ ),
+ ],
+)]
+class Chicken
+{
+ #[ORM\Id]
+ #[ORM\GeneratedValue]
+ #[ORM\Column(type: 'integer')]
+ private ?int $id = null;
+
+ #[ORM\Column(type: 'string', length: 255)]
+ private string $name;
+
+ #[ORM\ManyToOne(targetEntity: ChickenCoop::class, inversedBy: 'chickens')]
+ #[ORM\JoinColumn(nullable: false)]
+ private ChickenCoop $chickenCoop;
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getName(): ?string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): self
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ public function getChickenCoop(): ?ChickenCoop
+ {
+ return $this->chickenCoop;
+ }
+
+ public function setChickenCoop(?ChickenCoop $chickenCoop): self
+ {
+ $this->chickenCoop = $chickenCoop;
+
+ return $this;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Entity/ChickenCoop.php b/tests/Fixtures/TestBundle/Entity/ChickenCoop.php
new file mode 100644
index 00000000000..410261adb84
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Entity/ChickenCoop.php
@@ -0,0 +1,76 @@
+
+ *
+ * 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;
+
+use ApiPlatform\Doctrine\Orm\Filter\IriFilter;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\QueryParameter;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ORM\Mapping as ORM;
+
+#[ORM\Entity]
+#[GetCollection(
+ normalizationContext: ['hydra_prefix' => false],
+ parameters: ['chickens' => new QueryParameter(filter: new IriFilter())]
+)]
+class ChickenCoop
+{
+ #[ORM\Id]
+ #[ORM\GeneratedValue]
+ #[ORM\Column(type: 'integer')]
+ private ?int $id = null;
+
+ #[ORM\OneToMany(targetEntity: Chicken::class, mappedBy: 'chickenCoop', cascade: ['persist'])]
+ private Collection $chickens;
+
+ public function __construct()
+ {
+ $this->chickens = new ArrayCollection();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getChickens(): Collection
+ {
+ return $this->chickens;
+ }
+
+ public function addChicken(Chicken $chicken): self
+ {
+ if (!$this->chickens->contains($chicken)) {
+ $this->chickens[] = $chicken;
+ $chicken->setChickenCoop($this);
+ }
+
+ return $this;
+ }
+
+ public function removeChicken(Chicken $chicken): self
+ {
+ if ($this->chickens->removeElement($chicken)) {
+ if ($chicken->getChickenCoop() === $this) {
+ $chicken->setChickenCoop(null);
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/tests/Functional/Parameters/ExactFilterTest.php b/tests/Functional/Parameters/ExactFilterTest.php
new file mode 100644
index 00000000000..c312c6db15a
--- /dev/null
+++ b/tests/Functional/Parameters/ExactFilterTest.php
@@ -0,0 +1,149 @@
+
+ *
+ * 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\Functional\Parameters;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Chicken as DocumentChicken;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ChickenCoop as DocumentChickenCoop;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Chicken;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ChickenCoop;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+use Doctrine\ODM\MongoDB\MongoDBException;
+use PHPUnit\Framework\Attributes\DataProvider;
+use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
+
+final class ExactFilterTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [Chicken::class, ChickenCoop::class];
+ }
+
+ /**
+ * @throws \Throwable
+ */
+ protected function setUp(): void
+ {
+ $entities = $this->isMongoDB()
+ ? [DocumentChicken::class, DocumentChickenCoop::class]
+ : [Chicken::class, ChickenCoop::class];
+
+ $this->recreateSchema($entities);
+ $this->loadFixtures();
+ }
+
+ /**
+ * @throws ServerExceptionInterface
+ * @throws RedirectionExceptionInterface
+ * @throws DecodingExceptionInterface
+ * @throws ClientExceptionInterface
+ * @throws TransportExceptionInterface
+ */
+ #[DataProvider('exactSearchFilterProvider')]
+ public function testExactSearchFilter(string $url, int $expectedCount, array $expectedNames): void
+ {
+ $response = self::createClient()->request('GET', $url);
+ $this->assertResponseIsSuccessful();
+
+ $responseData = $response->toArray();
+ $filteredItems = $responseData['hydra:member'];
+
+ $this->assertCount($expectedCount, $filteredItems, \sprintf('Expected %d items for URL %s', $expectedCount, $url));
+
+ $names = array_map(fn ($chicken) => $chicken['name'], $filteredItems);
+ sort($names);
+ sort($expectedNames);
+
+ $this->assertSame($expectedNames, $names, 'The names do not match the expected values.');
+ }
+
+ public static function exactSearchFilterProvider(): \Generator
+ {
+ yield 'filter by exact name "Gertrude"' => [
+ '/chickens?name=Gertrude',
+ 1,
+ ['Gertrude'],
+ ];
+
+ yield 'filter by a non-existent name' => [
+ '/chickens?name=Kevin',
+ 0,
+ [],
+ ];
+
+ yield 'filter by exact coop id' => [
+ '/chickens?chickenCoop=1',
+ 1,
+ ['Gertrude'],
+ ];
+
+ yield 'filter by coop id and correct name' => [
+ '/chickens?chickenCoop=1&name=Gertrude',
+ 1,
+ ['Gertrude'],
+ ];
+
+ yield 'filter by coop id and incorrect name' => [
+ '/chickens?chickenCoop=1&name=Henriette',
+ 0,
+ [],
+ ];
+ }
+
+ /**
+ * @throws \Throwable
+ * @throws MongoDBException
+ */
+ private function loadFixtures(): void
+ {
+ $manager = $this->getManager();
+
+ $chickenClass = $this->isMongoDB() ? DocumentChicken::class : Chicken::class;
+ $coopClass = $this->isMongoDB() ? DocumentChickenCoop::class : ChickenCoop::class;
+
+ $chickenCoop1 = new $coopClass();
+ $chickenCoop2 = new $coopClass();
+
+ $chicken1 = new $chickenClass();
+ $chicken1->setName('Gertrude');
+ $chicken1->setChickenCoop($chickenCoop1);
+
+ $chicken2 = new $chickenClass();
+ $chicken2->setName('Henriette');
+ $chicken2->setChickenCoop($chickenCoop2);
+
+ if (method_exists($chickenCoop1, 'addChicken')) {
+ $chickenCoop1->addChicken($chicken1);
+ $chickenCoop2->addChicken($chicken2);
+ }
+
+ $manager->persist($chickenCoop1);
+ $manager->persist($chickenCoop2);
+ $manager->persist($chicken1);
+ $manager->persist($chicken2);
+
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/Parameters/IriFilterTest.php b/tests/Functional/Parameters/IriFilterTest.php
new file mode 100644
index 00000000000..a2b32e0bba0
--- /dev/null
+++ b/tests/Functional/Parameters/IriFilterTest.php
@@ -0,0 +1,92 @@
+
+ *
+ * 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\Functional\Parameters;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Chicken as DocumentChicken;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ChickenCoop as DocumentChickenCoop;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Chicken;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ChickenCoop;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+use Doctrine\ODM\MongoDB\MongoDBException;
+
+final class IriFilterTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [ChickenCoop::class, Chicken::class];
+ }
+
+ public function testIriFilter(): void
+ {
+ $client = $this->createClient();
+ $res = $client->request('GET', '/chicken_coops?chickens=/chickens/2')->toArray();
+ $this->assertCount(1, $res['member']);
+ $this->assertEquals(['/chickens/2'], $res['member'][0]['chickens']);
+ }
+
+ public function testIriFilterMultiple(): void
+ {
+ $client = $this->createClient();
+ $res = $client->request('GET', '/chicken_coops?chickens[]=/chickens/2&chickens[]=/chickens/1')->toArray();
+ $this->assertCount(2, $res['member']);
+ }
+
+ /**
+ * @throws \Throwable
+ */
+ protected function setUp(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DocumentChicken::class : Chicken::class, $this->isMongoDB() ? DocumentChickenCoop::class : ChickenCoop::class]);
+ $this->loadFixtures();
+ }
+
+ /**
+ * @throws \Throwable
+ * @throws MongoDBException
+ */
+ private function loadFixtures(): void
+ {
+ $manager = $this->getManager();
+
+ $chickenCoop1 = new ($this->isMongoDB() ? DocumentChickenCoop::class : ChickenCoop::class)();
+ $chickenCoop2 = new ($this->isMongoDB() ? DocumentChickenCoop::class : ChickenCoop::class)();
+
+ $chicken1 = new ($this->isMongoDB() ? DocumentChicken::class : Chicken::class)();
+ $chicken1->setName('Gertrude');
+ $chicken1->setChickenCoop($chickenCoop1);
+
+ $chicken2 = new ($this->isMongoDB() ? DocumentChicken::class : Chicken::class)();
+ $chicken2->setName('Henriette');
+ $chicken2->setChickenCoop($chickenCoop2);
+
+ $chickenCoop1->addChicken($chicken1);
+ $chickenCoop2->addChicken($chicken2);
+
+ $manager->persist($chicken1);
+ $manager->persist($chicken2);
+ $manager->persist($chickenCoop1);
+ $manager->persist($chickenCoop2);
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/Parameters/PartialSearchFilterTest.php b/tests/Functional/Parameters/PartialSearchFilterTest.php
new file mode 100644
index 00000000000..8c235fcfcba
--- /dev/null
+++ b/tests/Functional/Parameters/PartialSearchFilterTest.php
@@ -0,0 +1,143 @@
+
+ *
+ * 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\Functional\Parameters;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Chicken as DocumentChicken;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ChickenCoop as DocumentChickenCoop;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Chicken;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ChickenCoop;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+use Doctrine\ODM\MongoDB\MongoDBException;
+use PHPUnit\Framework\Attributes\DataProvider;
+use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
+
+final class PartialSearchFilterTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [Chicken::class, ChickenCoop::class];
+ }
+
+ /**
+ * @throws \Throwable
+ */
+ protected function setUp(): void
+ {
+ $entities = $this->isMongoDB()
+ ? [DocumentChicken::class, DocumentChickenCoop::class]
+ : [Chicken::class, ChickenCoop::class];
+
+ $this->recreateSchema($entities);
+ $this->loadFixtures();
+ }
+
+ /**
+ * @throws ServerExceptionInterface
+ * @throws RedirectionExceptionInterface
+ * @throws DecodingExceptionInterface
+ * @throws ClientExceptionInterface
+ * @throws TransportExceptionInterface
+ */
+ #[DataProvider('partialSearchFilterProvider')]
+ public function testPartialSearchFilter(string $url, int $expectedCount, array $expectedNames): void
+ {
+ $response = self::createClient()->request('GET', $url);
+ $this->assertResponseIsSuccessful();
+
+ $responseData = $response->toArray();
+ $filteredItems = $responseData['hydra:member'];
+
+ $this->assertCount($expectedCount, $filteredItems, \sprintf('Expected %d items for URL %s', $expectedCount, $url));
+
+ $names = array_map(fn ($chicken) => $chicken['name'], $filteredItems);
+ sort($names);
+ sort($expectedNames);
+
+ $this->assertSame($expectedNames, $names, 'The returned names do not match the expected values.');
+ }
+
+ public static function partialSearchFilterProvider(): \Generator
+ {
+ yield 'filter by partial name "ertrude"' => [
+ '/chickens?namePartial=ertrude',
+ 1,
+ ['Gertrude'],
+ ];
+
+ yield 'filter by partial name "riette"' => [
+ '/chickens?namePartial=riette',
+ 1,
+ ['Henriette'],
+ ];
+
+ yield 'filter by partial name "e" (should match both)' => [
+ '/chickens?namePartial=e',
+ 2,
+ ['Gertrude', 'Henriette'],
+ ];
+
+ yield 'filter by partial name with no matching entities' => [
+ '/chickens?namePartial=Zebra',
+ 0,
+ [],
+ ];
+ }
+
+ /**
+ * @throws \Throwable
+ * @throws MongoDBException
+ */
+ private function loadFixtures(): void
+ {
+ $manager = $this->getManager();
+
+ $chickenClass = $this->isMongoDB() ? DocumentChicken::class : Chicken::class;
+ $coopClass = $this->isMongoDB() ? DocumentChickenCoop::class : ChickenCoop::class;
+
+ $chickenCoop1 = new $coopClass();
+ $chickenCoop2 = new $coopClass();
+
+ $chicken1 = new $chickenClass();
+ $chicken1->setName('Gertrude');
+ $chicken1->setChickenCoop($chickenCoop1);
+
+ $chicken2 = new $chickenClass();
+ $chicken2->setName('Henriette');
+ $chicken2->setChickenCoop($chickenCoop2);
+
+ if (method_exists($chickenCoop1, 'addChicken')) {
+ $chickenCoop1->addChicken($chicken1);
+ $chickenCoop2->addChicken($chicken2);
+ }
+
+ $manager->persist($chickenCoop1);
+ $manager->persist($chickenCoop2);
+ $manager->persist($chicken1);
+ $manager->persist($chicken2);
+
+ $manager->flush();
+ }
+}