Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e04c3c0
feat: iri search filter
soyuka Apr 10, 2025
cab6bd6
fix: apply last reviews made in th latest pr
vinceAmstoutz May 2, 2025
afd57e3
feat(doctrine): add ORM ExactFilter
vinceAmstoutz May 5, 2025
ed57ace
feat(doctrine): add ORM PartialSearchFilter
vinceAmstoutz Jun 5, 2025
79bfc31
refactor(test): unifies fixtures for filter
vinceAmstoutz Jun 8, 2025
e1a676d
feat(doctrine): finish ODM IriFilter
vinceAmstoutz Jun 8, 2025
fff2121
feat(doctrine): add ODM ExactFilter
vinceAmstoutz Jun 8, 2025
8b6d41f
feat(doctrine): add ODM PartialSearchFilter
vinceAmstoutz Jun 8, 2025
d9911c2
refactor(doctrine): remove dead code
vinceAmstoutz Jun 8, 2025
7943174
fix(test): deprecation on kernel will not always be booted
vinceAmstoutz Jun 12, 2025
0c304f3
feat(mongodb): ODM support for new filters
vinceAmstoutz Jun 12, 2025
cbab08b
refactor(state): remove duplicate symbols
vinceAmstoutz Jul 2, 2025
98109cf
cs(state): phpdoc rather than asserting
vinceAmstoutz Jul 2, 2025
a471dfa
refactor(doctrine): introduce OpenApiFilterTrait.php
vinceAmstoutz Jul 3, 2025
b5e6ac8
feat(doctrine): add OrFilter for ORM and ODM
vinceAmstoutz Jul 4, 2025
11f9298
cs(doctrine): fix typing issues
vinceAmstoutz Jul 4, 2025
ec42ce2
fix(doctrine): add authorship
vinceAmstoutz Jul 4, 2025
d065380
fix(test): apply review requested changes
vinceAmstoutz Aug 20, 2025
9f82c5a
feat(mongodb): handle aggregation for PartialSearchFilter
vinceAmstoutz Aug 22, 2025
74a21ef
fix(test): apply review requested changes
vinceAmstoutz Aug 22, 2025
a27de22
fix(mongodb): start fixing filters when relations
vinceAmstoutz Aug 25, 2025
da4c493
fix(mongodb): add LoggerAwareTrait and ManagerRegistryAwareTrait, imp…
soyuka Aug 25, 2025
61e1b9a
fix(mongodb): enhance ExactFilter and IriFilter to better handle rela…
vinceAmstoutz Aug 26, 2025
57ed37e
fixes
soyuka Aug 26, 2025
7349cec
more
soyuka Aug 27, 2025
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
25 changes: 25 additions & 0 deletions src/Doctrine/Common/Filter/LoggerAwareInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\Common\Filter;

use Psr\Log\LoggerInterface;

interface LoggerAwareInterface
{
public function hasLogger(): bool;

public function getLogger(): LoggerInterface;

public function setLogger(LoggerInterface $logger): void;
}
37 changes: 37 additions & 0 deletions src/Doctrine/Common/Filter/LoggerAwareTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\Common\Filter;

use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

trait LoggerAwareTrait
{
private ?LoggerInterface $logger = null;

public function hasLogger(): bool
{
return $this->logger instanceof LoggerInterface;
}

public function getLogger(): LoggerInterface
{
return $this->logger ??= new NullLogger();
}

public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
}
41 changes: 41 additions & 0 deletions src/Doctrine/Common/Filter/ManagerRegistryAwareTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\Common\Filter;

use ApiPlatform\Metadata\Exception\RuntimeException;
use Doctrine\Persistence\ManagerRegistry;

trait ManagerRegistryAwareTrait
{
private ?ManagerRegistry $managerRegistry = null;

public function hasManagerRegistry(): bool
{
return $this->managerRegistry instanceof ManagerRegistry;
}

public function getManagerRegistry(): ManagerRegistry
{
if (!$this->hasManagerRegistry()) {
throw new RuntimeException('ManagerRegistry must be initialized before accessing it.');
}

return $this->managerRegistry;
}

public function setManagerRegistry(ManagerRegistry $managerRegistry): void
{
$this->managerRegistry = $managerRegistry;
}
}
28 changes: 28 additions & 0 deletions src/Doctrine/Common/Filter/OpenApiFilterTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\Common\Filter;

use ApiPlatform\Metadata\Parameter;
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;

/**
* @author Vincent Amstoutz <[email protected]>
*/
trait OpenApiFilterTrait
{
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
{
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
}
}
16 changes: 15 additions & 1 deletion src/Doctrine/Odm/Extension/ParameterExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Doctrine\Odm\Extension;

use ApiPlatform\Doctrine\Common\Filter\LoggerAwareInterface;
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface;
use ApiPlatform\Doctrine\Common\ParameterValueExtractorTrait;
use ApiPlatform\Doctrine\Odm\Filter\AbstractFilter;
Expand All @@ -22,6 +23,7 @@
use Doctrine\Bundle\MongoDBBundle\ManagerRegistry;
use Doctrine\ODM\MongoDB\Aggregation\Builder;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

/**
* Reads operation parameters and execute its filter.
Expand All @@ -35,6 +37,7 @@ final class ParameterExtension implements AggregationCollectionExtensionInterfac
public function __construct(
private readonly ContainerInterface $filterLocator,
private readonly ?ManagerRegistry $managerRegistry = null,
private readonly ?LoggerInterface $logger = null,
) {
}

Expand Down Expand Up @@ -67,6 +70,10 @@ private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass
$filter->setManagerRegistry($this->managerRegistry);
}

if ($this->logger && $filter instanceof LoggerAwareInterface && !$filter->hasLogger()) {
$filter->setLogger($this->logger);
}

if ($filter instanceof AbstractFilter && !$filter->getProperties()) {
$propertyKey = $parameter->getProperty() ?? $parameter->getKey();

Expand All @@ -82,12 +89,19 @@ private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass
$filter->setProperties($properties ?? []);
}

$filterContext = ['filters' => $values, 'parameter' => $parameter];
$filterContext = ['filters' => $values, 'parameter' => $parameter, 'match' => $context['match'] ?? null];
$filter->apply($aggregationBuilder, $resourceClass, $operation, $filterContext);
// update by reference
if (isset($filterContext['mongodb_odm_sort_fields'])) {
$context['mongodb_odm_sort_fields'] = $filterContext['mongodb_odm_sort_fields'];
}
if (isset($filterContext['match'])) {
$context['match'] = $filterContext['match'];
}
}

if (isset($context['match'])) {
$aggregationBuilder->match()->addAnd($context['match']);
}
}

Expand Down
86 changes: 86 additions & 0 deletions src/Doctrine/Odm/Filter/ExactFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\Doctrine\Common\Filter\ManagerRegistryAwareInterface;
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait;
use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait;
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ODM\MongoDB\Aggregation\Builder;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\LockException;
use Doctrine\ODM\MongoDB\Mapping\MappingException;

/**
* @author Vincent Amstoutz <[email protected]>
*/
final class ExactFilter implements FilterInterface, OpenApiParameterFilterInterface, ManagerRegistryAwareInterface
{
use BackwardCompatibleFilterDescriptionTrait;
use ManagerRegistryAwareTrait;
use OpenApiFilterTrait;

/**
* @throws MappingException
* @throws LockException
*/
public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
{
$parameter = $context['parameter'];
$property = $parameter->getProperty();
$value = $parameter->getValue();
$operator = $context['operator'] ?? 'addAnd';
$match = $context['match'] = $context['match'] ??
$aggregationBuilder
->matchExpr();

$documentManager = $this->getManagerRegistry()->getManagerForClass($resourceClass);
if (!$documentManager instanceof DocumentManager) {
return;
}

$classMetadata = $documentManager->getClassMetadata($resourceClass);

if (!$classMetadata->hasReference($property)) {
$match
->{$operator}($aggregationBuilder->matchExpr()->field($property)->{is_iterable($value) ? 'in' : 'equals'}($value));

return;
}

$mapping = $classMetadata->getFieldMapping($property);
$method = $classMetadata->isSingleValuedAssociation($property) ? 'references' : 'includesReferenceTo';

if (is_iterable($value)) {
$or = $aggregationBuilder->matchExpr();

foreach ($value as $v) {
$or->addOr($aggregationBuilder->matchExpr()->field($property)->{$method}($documentManager->getPartialReference($mapping['targetDocument'], $v)));
}

$match->{$operator}($or);

return;
}

$match
->{$operator}(
$aggregationBuilder->matchExpr()
->field($property)
->{$method}($documentManager->getPartialReference($mapping['targetDocument'], $value))
);
}
}
3 changes: 3 additions & 0 deletions src/Doctrine/Odm/Filter/FilterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use ApiPlatform\Metadata\FilterInterface as BaseFilterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Parameter;
use Doctrine\ODM\MongoDB\Aggregation\Builder;

/**
Expand All @@ -26,6 +27,8 @@ interface FilterInterface extends BaseFilterInterface
{
/**
* Applies the filter.
*
* @param array|array{filters?: array<string, mixed>|array, parameter?: Parameter, mongodb_odm_sort_fields?: array, ...} $context
*/
public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void;
}
62 changes: 62 additions & 0 deletions src/Doctrine/Odm/Filter/FreeTextQueryFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\Doctrine\Common\Filter\LoggerAwareInterface;
use ApiPlatform\Doctrine\Common\Filter\LoggerAwareTrait;
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface;
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait;
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
use ApiPlatform\Metadata\Operation;
use Doctrine\ODM\MongoDB\Aggregation\Builder;

final class FreeTextQueryFilter implements FilterInterface, ManagerRegistryAwareInterface, LoggerAwareInterface
{
use BackwardCompatibleFilterDescriptionTrait;
use LoggerAwareTrait;
use ManagerRegistryAwareTrait;

/**
* @param list<string> $properties an array of properties, defaults to `parameter->getProperties()`
*/
public function __construct(private readonly FilterInterface $filter, private readonly ?array $properties = null)
{
}

public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
{
if ($this->filter instanceof ManagerRegistryAwareInterface) {
$this->filter->setManagerRegistry($this->getManagerRegistry());
}

if ($this->filter instanceof LoggerAwareInterface) {
$this->filter->setLogger($this->getLogger());
}

$parameter = $context['parameter'];
foreach ($this->properties ?? $parameter->getProperties() ?? [] as $property) {
$newContext = ['parameter' => $parameter->withProperty($property), 'match' => $context['match'] ?? $aggregationBuilder->match()->expr()] + $context;
$this->filter->apply(
$aggregationBuilder,
$resourceClass,
$operation,
$newContext,
);

if (isset($newContext['match'])) {
$context['match'] = $newContext['match'];
}
}
}
}
Loading
Loading