Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
8 changes: 7 additions & 1 deletion src/Doctrine/Odm/Extension/ParameterExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,18 @@ private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass

$this->configureFilter($filter, $parameter);

$previousFilters = $context['filters'] ?? null;
$context['filters'] = $values;
$context['parameter'] = $parameter;

$filter->apply($aggregationBuilder, $resourceClass, $operation, $context);

unset($context['filters'], $context['parameter']);
unset($context['parameter']);
if (null !== $previousFilters) {
$context['filters'] = $previousFilters;
} else {
unset($context['filters']);
}
}

if (isset($context['match'])) {
Expand Down
142 changes: 142 additions & 0 deletions src/Doctrine/Odm/Filter/ComparisonFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?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\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\Doctrine\Common\Filter\OpenApiFilterTrait;
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
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;

/**
* Decorates an equality filter (ExactFilter) to add comparison operators (gt, gte, lt, lte).
*
* @experimental
*/
Comment thread
soyuka marked this conversation as resolved.
final class ComparisonFilter implements FilterInterface, OpenApiParameterFilterInterface, JsonSchemaFilterInterface, ManagerRegistryAwareInterface, LoggerAwareInterface
{
use BackwardCompatibleFilterDescriptionTrait;
use LoggerAwareTrait;
use ManagerRegistryAwareTrait;
use OpenApiFilterTrait;

private const OPERATORS = [
'gt' => 'gt',
'gte' => 'gte',
'lt' => 'lt',
'lte' => 'lte',
];

public const ALLOWED_COMPARISON_METHODS = ['equals', 'gt', 'gte', 'lt', 'lte'];

public function __construct(private readonly FilterInterface $filter)
{
}

/**
* @param-out array<string, mixed> $context
*/
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'];
$values = $parameter->getValue();

if (!\is_array($values)) {
return;
}

foreach ($values as $operator => $value) {
if ('' === $value || null === $value) {
continue;
}

if (isset(self::OPERATORS[$operator])) {
$this->applyOperator($aggregationBuilder, $resourceClass, $operation, $context, $parameter, self::OPERATORS[$operator], $value);
}
}
}

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

return [
new OpenApiParameter(name: "{$key}[gt]", in: $in, schema: $schema),
new OpenApiParameter(name: "{$key}[gte]", in: $in, schema: $schema),
new OpenApiParameter(name: "{$key}[lt]", in: $in, schema: $schema),
new OpenApiParameter(name: "{$key}[lte]", in: $in, schema: $schema),
];
}

public function getSchema(Parameter $parameter): array
{
$innerSchema = $this->getInnerSchema($parameter);

return [
'type' => 'object',
'properties' => [
'gt' => $innerSchema,
'gte' => $innerSchema,
'lt' => $innerSchema,
'lte' => $innerSchema,
],
];
}

/**
* @param array<string, mixed> $context
*
* @param-out array<string, mixed> $context
*/
private function applyOperator(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation, array &$context, Parameter $parameter, string $comparisonMethod, mixed $value): void
{
if (!\is_string($value) && !is_numeric($value) && !$value instanceof \DateTimeInterface) {
return;
}

$subParameter = (clone $parameter)->setValue($value);
$newContext = ['comparisonMethod' => $comparisonMethod, 'parameter' => $subParameter] + $context;
$this->filter->apply($aggregationBuilder, $resourceClass, $operation, $newContext);
if (isset($newContext['match'])) {
$context['match'] = $newContext['match'];
}
}

private function getInnerSchema(Parameter $parameter): array
{
if ($this->filter instanceof JsonSchemaFilterInterface) {
return $this->filter->getSchema($parameter);
}

return ['type' => 'string'];
}
}
6 changes: 5 additions & 1 deletion src/Doctrine/Odm/Filter/ExactFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Opera
$classMetadata = $documentManager->getClassMetadata($resourceClass);

if (!$classMetadata->hasReference($property)) {
$comparisonMethod = $context['comparisonMethod'] ?? (is_iterable($value) ? 'in' : 'equals');
if (!\in_array($comparisonMethod, ComparisonFilter::ALLOWED_COMPARISON_METHODS, true) && 'in' !== $comparisonMethod) {
throw new InvalidArgumentException(\sprintf('Unsupported comparison method "%s".', $comparisonMethod));
}
$match
->{$operator}($aggregationBuilder->matchExpr()->field($property)->{is_iterable($value) ? 'in' : 'equals'}($value));
->{$operator}($aggregationBuilder->matchExpr()->field($property)->{$comparisonMethod}($value));

return;
}
Expand Down
26 changes: 19 additions & 7 deletions src/Doctrine/Orm/Filter/AbstractUuidFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,14 @@ private function filterProperty(Parameter $parameter, QueryBuilder $queryBuilder

$metadata = $this->getClassMetadata($targetResourceClass);

$operator = $context['operator'] ?? '=';
if (!\in_array($operator, ComparisonFilter::ALLOWED_DQL_OPERATORS, true)) {
throw new InvalidArgumentException(\sprintf('Unsupported operator "%s".', $operator));
}

if ($metadata->hasField($field)) {
$value = $this->convertValuesToTheDatabaseRepresentation($queryBuilder, $this->getDoctrineFieldType($field, $targetResourceClass), $value);
$this->addWhere($queryBuilder, $queryNameGenerator, $alias, $field, $value);
$this->addWhere($queryBuilder, $queryNameGenerator, $alias, $field, $value, $operator, $context);

return;
}
Expand Down Expand Up @@ -129,7 +134,7 @@ private function filterProperty(Parameter $parameter, QueryBuilder $queryBuilder
}

$value = $this->convertValuesToTheDatabaseRepresentation($queryBuilder, $doctrineTypeField, $value);
$this->addWhere($queryBuilder, $queryNameGenerator, $associationAlias, $associationField, $value);
$this->addWhere($queryBuilder, $queryNameGenerator, $associationAlias, $associationField, $value, $operator, $context);
}

/**
Expand Down Expand Up @@ -162,21 +167,28 @@ private function convertValuesToTheDatabaseRepresentation(QueryBuilder $queryBui
/**
* Adds where clause.
*/
private function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, mixed $value): void
private function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, mixed $value, string $operator = '=', array $context = []): void
{
$valueParameter = ':'.$queryNameGenerator->generateParameterName($field);
$aliasedField = \sprintf('%s.%s', $alias, $field);
$whereClause = $context['whereClause'] ?? 'andWhere';

if (!\is_array($value)) {
$queryBuilder
->andWhere($queryBuilder->expr()->eq($aliasedField, $valueParameter))
->setParameter($valueParameter, $value, $this->getDoctrineParameterType());
if ('=' === $operator) {
$queryBuilder
->{$whereClause}($queryBuilder->expr()->eq($aliasedField, $valueParameter))
->setParameter($valueParameter, $value, $this->getDoctrineParameterType());
} else {
$queryBuilder
->{$whereClause}(\sprintf('%s %s %s', $aliasedField, $operator, $valueParameter))
->setParameter($valueParameter, $value, $this->getDoctrineParameterType());
}

return;
}

$queryBuilder
->andWhere($queryBuilder->expr()->in($aliasedField, $valueParameter))
->{$whereClause}($queryBuilder->expr()->in($aliasedField, $valueParameter))
->setParameter($valueParameter, $value, $this->getDoctrineArrayParameterType());
}

Expand Down
137 changes: 137 additions & 0 deletions src/Doctrine/Orm/Filter/ComparisonFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?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\Orm\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\Doctrine\Common\Filter\OpenApiFilterTrait;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
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\ORM\QueryBuilder;

/**
* Decorates an equality filter (ExactFilter, UuidFilter) to add comparison operators (gt, gte, lt, lte).
*
* @experimental
*/
Comment thread
soyuka marked this conversation as resolved.
final class ComparisonFilter implements FilterInterface, OpenApiParameterFilterInterface, JsonSchemaFilterInterface, ManagerRegistryAwareInterface, LoggerAwareInterface
{
use BackwardCompatibleFilterDescriptionTrait;
use LoggerAwareTrait;
use ManagerRegistryAwareTrait;
use OpenApiFilterTrait;

private const OPERATORS = [
'gt' => '>',
'gte' => '>=',
'lt' => '<',
'lte' => '<=',
];

public const ALLOWED_DQL_OPERATORS = ['=', '>', '>=', '<', '<=', '!=', '<>'];

public function __construct(private readonly FilterInterface $filter)
{
}

public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, 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'];
$values = $parameter->getValue();

if (!\is_array($values)) {
return;
}

foreach ($values as $operator => $value) {
if ('' === $value || null === $value) {
continue;
}

if (isset(self::OPERATORS[$operator])) {
$this->applyOperator($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context, $parameter, self::OPERATORS[$operator], $value);
}
}
}

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

return [
new OpenApiParameter(name: "{$key}[gt]", in: $in, schema: $schema),
new OpenApiParameter(name: "{$key}[gte]", in: $in, schema: $schema),
new OpenApiParameter(name: "{$key}[lt]", in: $in, schema: $schema),
new OpenApiParameter(name: "{$key}[lte]", in: $in, schema: $schema),
];
}

public function getSchema(Parameter $parameter): array
{
$innerSchema = $this->getInnerSchema($parameter);

return [
'type' => 'object',
'properties' => [
'gt' => $innerSchema,
'gte' => $innerSchema,
'lt' => $innerSchema,
'lte' => $innerSchema,
],
];
}

private function applyOperator(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation, array $context, Parameter $parameter, string $operator, mixed $value): void
{
if (!\is_string($value) && !is_numeric($value) && !$value instanceof \DateTimeInterface) {
return;
}

$subParameter = (clone $parameter)->setValue($value);
$this->filter->apply(
$queryBuilder,
$queryNameGenerator,
$resourceClass,
$operation,
['operator' => $operator, 'parameter' => $subParameter] + $context
);
}

private function getInnerSchema(Parameter $parameter): array
{
if ($this->filter instanceof JsonSchemaFilterInterface) {
return $this->filter->getSchema($parameter);
}

return ['type' => 'string'];
}
}
6 changes: 5 additions & 1 deletion src/Doctrine/Orm/Filter/ExactFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,12 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q
$queryBuilder
->{$context['whereClause'] ?? 'andWhere'}(\sprintf('%s.%s IN (:%s)', $alias, $property, $parameterName));
} else {
$operator = $context['operator'] ?? '=';
if (!\in_array($operator, ComparisonFilter::ALLOWED_DQL_OPERATORS, true)) {
throw new InvalidArgumentException(\sprintf('Unsupported operator "%s".', $operator));
}
$queryBuilder
->{$context['whereClause'] ?? 'andWhere'}(\sprintf('%s.%s = :%s', $alias, $property, $parameterName));
->{$context['whereClause'] ?? 'andWhere'}(\sprintf('%s.%s %s :%s', $alias, $property, $operator, $parameterName));
}

$queryBuilder->setParameter($parameterName, $value);
Expand Down
Loading
Loading