Skip to content

Commit

Permalink
feat(doctrine): doctrine filters like laravel eloquent filters (api-p…
Browse files Browse the repository at this point in the history
…latform#6775)

* feat(doctrine): doctrine filters like laravel eloquent filters

* fix: allow multiple validation with :property placeholder

* fix: correct escape filter condition

* fix: remove duplicated block

---------

Co-authored-by: soyuka <soyuka@users.noreply.github.com>
  • Loading branch information
2 people authored and jonerickson committed Feb 21, 2025
1 parent 8697f21 commit 509939d
Show file tree
Hide file tree
Showing 42 changed files with 1,994 additions and 74 deletions.
25 changes: 25 additions & 0 deletions src/Doctrine/Common/Filter/ManagerRegistryAwareInterface.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 <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\Doctrine\Common\Filter;

use Doctrine\Persistence\ManagerRegistry;

interface ManagerRegistryAwareInterface
{
public function hasManagerRegistry(): bool;

public function getManagerRegistry(): ManagerRegistry;

public function setManagerRegistry(ManagerRegistry $managerRegistry): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?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\Doctrine\Common\Filter;

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

trait PropertyPlaceholderOpenApiParameterTrait
{
/**
* @return array<OpenApiParameter>|null
*/
public function getOpenApiParameters(Parameter $parameter): ?array
{
if (str_contains($parameter->getKey(), ':property')) {
$parameters = [];
$key = str_replace('[:property]', '', $parameter->getKey());
foreach (array_keys($parameter->getExtraProperties()['_properties'] ?? []) as $property) {
$parameters[] = new OpenApiParameter(name: \sprintf('%s[%s]', $key, $property), in: 'query');
}

return $parameters;
}

return null;
}
}
35 changes: 29 additions & 6 deletions src/Doctrine/Odm/Filter/AbstractFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@

namespace ApiPlatform\Doctrine\Odm\Filter;

use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface;
use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface;
use ApiPlatform\Doctrine\Common\PropertyHelperTrait;
use ApiPlatform\Doctrine\Odm\PropertyHelperTrait as MongoDbOdmPropertyHelperTrait;
use ApiPlatform\Metadata\Exception\RuntimeException;
use ApiPlatform\Metadata\Operation;
use Doctrine\ODM\MongoDB\Aggregation\Builder;
use Doctrine\Persistence\ManagerRegistry;
Expand All @@ -30,14 +32,18 @@
*
* @author Alan Poulain <contact@alanpoulain.eu>
*/
abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface
abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface, ManagerRegistryAwareInterface
{
use MongoDbOdmPropertyHelperTrait;
use PropertyHelperTrait;
protected LoggerInterface $logger;

public function __construct(protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, protected ?NameConverterInterface $nameConverter = null)
{
public function __construct(
protected ?ManagerRegistry $managerRegistry = null,
?LoggerInterface $logger = null,
protected ?array $properties = null,
protected ?NameConverterInterface $nameConverter = null,
) {
$this->logger = $logger ?? new NullLogger();
}

Expand All @@ -56,18 +62,35 @@ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Opera
*/
abstract protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void;

protected function getManagerRegistry(): ManagerRegistry
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;
}

protected function getProperties(): ?array
public function setManagerRegistry(ManagerRegistry $managerRegistry): void
{
$this->managerRegistry = $managerRegistry;
}

/**
* @return array<string, mixed>|null
*/
public function getProperties(): ?array
{
return $this->properties;
}

/**
* @param string[] $properties
* @param array<string, mixed> $properties
*/
public function setProperties(array $properties): void
{
Expand Down
12 changes: 11 additions & 1 deletion src/Doctrine/Odm/Filter/BooleanFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
namespace ApiPlatform\Doctrine\Odm\Filter;

use ApiPlatform\Doctrine\Common\Filter\BooleanFilterTrait;
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Parameter;
use Doctrine\ODM\MongoDB\Aggregation\Builder;
use Doctrine\ODM\MongoDB\Types\Type as MongoDbType;

Expand Down Expand Up @@ -104,7 +106,7 @@
* @author Teoh Han Hui <teohhanhui@gmail.com>
* @author Alan Poulain <contact@alanpoulain.eu>
*/
final class BooleanFilter extends AbstractFilter
final class BooleanFilter extends AbstractFilter implements JsonSchemaFilterInterface
{
use BooleanFilterTrait;

Expand Down Expand Up @@ -139,4 +141,12 @@ protected function filterProperty(string $property, $value, Builder $aggregation

$aggregationBuilder->match()->field($matchField)->equals($value);
}

/**
* @return array<string, string>
*/
public function getSchema(Parameter $parameter): array
{
return ['type' => 'boolean'];
}
}
50 changes: 38 additions & 12 deletions src/Doctrine/Odm/Filter/DateFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Common\Filter\DateFilterTrait;
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
use Doctrine\ODM\MongoDB\Aggregation\Builder;
use Doctrine\ODM\MongoDB\Types\Type as MongoDbType;

Expand Down Expand Up @@ -117,7 +122,7 @@
* @author Théo FIDRY <theo.fidry@gmail.com>
* @author Alan Poulain <contact@alanpoulain.eu>
*/
final class DateFilter extends AbstractFilter implements DateFilterInterface
final class DateFilter extends AbstractFilter implements DateFilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
{
use DateFilterTrait;

Expand All @@ -129,11 +134,11 @@ final class DateFilter extends AbstractFilter implements DateFilterInterface
/**
* {@inheritdoc}
*/
protected function filterProperty(string $property, $values, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
{
// Expect $values to be an array having the period as keys and the date value as values
// Expect $value to be an array having the period as keys and the date value as values
if (
!\is_array($values)
!\is_array($value)
|| !$this->isPropertyEnabled($property, $resourceClass)
|| !$this->isPropertyMapped($property, $resourceClass)
|| !$this->isDateField($property, $resourceClass)
Expand All @@ -153,42 +158,42 @@ protected function filterProperty(string $property, $values, Builder $aggregatio
$aggregationBuilder->match()->field($matchField)->notEqual(null);
}

if (isset($values[self::PARAMETER_BEFORE])) {
if (isset($value[self::PARAMETER_BEFORE])) {
$this->addMatch(
$aggregationBuilder,
$matchField,
self::PARAMETER_BEFORE,
$values[self::PARAMETER_BEFORE],
$value[self::PARAMETER_BEFORE],
$nullManagement
);
}

if (isset($values[self::PARAMETER_STRICTLY_BEFORE])) {
if (isset($value[self::PARAMETER_STRICTLY_BEFORE])) {
$this->addMatch(
$aggregationBuilder,
$matchField,
self::PARAMETER_STRICTLY_BEFORE,
$values[self::PARAMETER_STRICTLY_BEFORE],
$value[self::PARAMETER_STRICTLY_BEFORE],
$nullManagement
);
}

if (isset($values[self::PARAMETER_AFTER])) {
if (isset($value[self::PARAMETER_AFTER])) {
$this->addMatch(
$aggregationBuilder,
$matchField,
self::PARAMETER_AFTER,
$values[self::PARAMETER_AFTER],
$value[self::PARAMETER_AFTER],
$nullManagement
);
}

if (isset($values[self::PARAMETER_STRICTLY_AFTER])) {
if (isset($value[self::PARAMETER_STRICTLY_AFTER])) {
$this->addMatch(
$aggregationBuilder,
$matchField,
self::PARAMETER_STRICTLY_AFTER,
$values[self::PARAMETER_STRICTLY_AFTER],
$value[self::PARAMETER_STRICTLY_AFTER],
$nullManagement
);
}
Expand Down Expand Up @@ -237,4 +242,25 @@ private function addMatch(Builder $aggregationBuilder, string $field, string $op

$aggregationBuilder->match()->addAnd($aggregationBuilder->matchExpr()->field($field)->operator($operatorValue[$operator], $value));
}

/**
* @return array<string, string>
*/
public function getSchema(Parameter $parameter): array
{
return ['type' => 'date'];
}

public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
{
$in = $parameter instanceof QueryParameter ? 'query' : 'header';
$key = $parameter->getKey();

return [
new OpenApiParameter(name: $key.'[after]', in: $in),
new OpenApiParameter(name: $key.'[before]', in: $in),
new OpenApiParameter(name: $key.'[strictly_after]', in: $in),
new OpenApiParameter(name: $key.'[strictly_before]', in: $in),
];
}
}
21 changes: 19 additions & 2 deletions src/Doctrine/Odm/Filter/ExistsFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@

use ApiPlatform\Doctrine\Common\Filter\ExistsFilterInterface;
use ApiPlatform\Doctrine\Common\Filter\ExistsFilterTrait;
use ApiPlatform\Doctrine\Common\Filter\PropertyPlaceholderOpenApiParameterTrait;
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Parameter;
use Doctrine\ODM\MongoDB\Aggregation\Builder;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Doctrine\Persistence\ManagerRegistry;
Expand Down Expand Up @@ -107,11 +111,12 @@
* @author Teoh Han Hui <teohhanhui@gmail.com>
* @author Alan Poulain <contact@alanpoulain.eu>
*/
final class ExistsFilter extends AbstractFilter implements ExistsFilterInterface
final class ExistsFilter extends AbstractFilter implements ExistsFilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
{
use ExistsFilterTrait;
use PropertyPlaceholderOpenApiParameterTrait;

public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, ?array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, ?NameConverterInterface $nameConverter = null)
public function __construct(?ManagerRegistry $managerRegistry = null, ?LoggerInterface $logger = null, ?array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, ?NameConverterInterface $nameConverter = null)
{
parent::__construct($managerRegistry, $logger, $properties, $nameConverter);

Expand All @@ -123,6 +128,13 @@ public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $
*/
public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
{
$parameter = $context['parameter'] ?? null;
if (null !== ($value = $context['filters'][$parameter?->getProperty()] ?? null)) {
$this->filterProperty($this->denormalizePropertyName($parameter->getProperty()), $value, $aggregationBuilder, $resourceClass, $operation, $context);

return;
}

foreach ($context['filters'][$this->existsParameterName] ?? [] as $property => $value) {
$this->filterProperty($this->denormalizePropertyName($property), $value, $aggregationBuilder, $resourceClass, $operation, $context);
}
Expand Down Expand Up @@ -167,4 +179,9 @@ protected function isNullableField(string $property, string $resourceClass): boo

return $metadata instanceof ClassMetadata && $metadata->hasField($field) ? $metadata->isNullable($field) : false;
}

public function getSchema(Parameter $parameter): array
{
return ['type' => 'boolean'];
}
}
9 changes: 8 additions & 1 deletion src/Doctrine/Odm/Filter/NumericFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
namespace ApiPlatform\Doctrine\Odm\Filter;

use ApiPlatform\Doctrine\Common\Filter\NumericFilterTrait;
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Parameter;
use Doctrine\ODM\MongoDB\Aggregation\Builder;
use Doctrine\ODM\MongoDB\Types\Type as MongoDbType;

Expand Down Expand Up @@ -104,7 +106,7 @@
* @author Teoh Han Hui <teohhanhui@gmail.com>
* @author Alan Poulain <contact@alanpoulain.eu>
*/
final class NumericFilter extends AbstractFilter
final class NumericFilter extends AbstractFilter implements JsonSchemaFilterInterface
{
use NumericFilterTrait;

Expand Down Expand Up @@ -163,4 +165,9 @@ protected function getType(?string $doctrineType = null): string

return 'int';
}

public function getSchema(Parameter $parameter): array
{
return ['type' => 'numeric'];
}
}
Loading

0 comments on commit 509939d

Please sign in to comment.