Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pagination): Add UuidRangeFilter to allow Uuid cursors #4689

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Next Next commit
Refactor RangeFilter to introduce a UuidRangeFilter
KDederichs committed Mar 22, 2022
commit 29897975d58cbbdcb464083656d7b6789087f868
8 changes: 4 additions & 4 deletions src/Doctrine/Common/Filter/RangeFilterTrait.php
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@
use Psr\Log\LoggerInterface;

/**
* Trait for filtering the collection by range.
* Trait for filtering the collection by range using numbers.
*
* @author Lee Siong Chan <[email protected]>
* @author Alan Poulain <[email protected]>
@@ -76,7 +76,7 @@ protected function getFilterDescription(string $fieldName, string $operator): ar
];
}

private function normalizeValues(array $values, string $property): ?array
protected function normalizeValues(array $values, string $property): ?array
{
$operators = [self::PARAMETER_BETWEEN, self::PARAMETER_GREATER_THAN, self::PARAMETER_GREATER_THAN_OR_EQUAL, self::PARAMETER_LESS_THAN, self::PARAMETER_LESS_THAN_OR_EQUAL];

@@ -100,7 +100,7 @@ private function normalizeValues(array $values, string $property): ?array
/**
* Normalize the values array for between operator.
*/
private function normalizeBetweenValues(array $values): ?array
protected function normalizeBetweenValues(array $values): ?array
{
if (2 !== \count($values)) {
$this->getLogger()->notice('Invalid filter ignored', [
@@ -126,7 +126,7 @@ private function normalizeBetweenValues(array $values): ?array
*
* @return int|float|null
*/
private function normalizeValue(string $value, string $operator)
protected function normalizeValue(string $value, string $operator)
{
if (!is_numeric($value)) {
$this->getLogger()->notice('Invalid filter ignored', [
71 changes: 71 additions & 0 deletions src/Doctrine/Common/Filter/UuidRangeFilterTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?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\Exception\InvalidArgumentException;

/**
* Trait for filtering the collection by range using UUIDs (UUID v6).
*
* @author Kai Dederichs <[email protected]>
*/
trait UuidRangeFilterTrait
{
use RangeFilterTrait;

/**
* {@inheritdoc}
*/
protected function normalizeBetweenValues(array $values): ?array
{
if (2 !== \count($values)) {
$this->getLogger()->notice('Invalid filter ignored', [
'exception' => new InvalidArgumentException(sprintf('Invalid format for "[%s]", expected "<min>..<max>"', self::PARAMETER_BETWEEN)),
]);

return null;
}

if (!$this->isValidUid($values[0]) || !$this->isValidUid($values[1])) {
$this->getLogger()->notice('Invalid filter ignored', [
'exception' => new InvalidArgumentException(sprintf('Invalid values for "[%s]" range, expected uuids', self::PARAMETER_BETWEEN)),
]);

return null;
}

return [$values[0], $values[1]];
}

/**
* Normalize the value.
*/
protected function normalizeValue(string $value, string $operator): ?string
{
if (!$this->isValidUid($value)) {
$this->getLogger()->notice('Invalid filter ignored', [
'exception' => new InvalidArgumentException(sprintf('Invalid value for "[%s]", expected number', $operator)),
]);

return null;
}

return $value;
}

private function isValidUid($potentialUid): bool
{
return \is_string($potentialUid) && preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$}Di', $potentialUid);
}
}
132 changes: 132 additions & 0 deletions src/Doctrine/Odm/Filter/AbstractRangeFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?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\RangeFilterInterface;
use Doctrine\ODM\MongoDB\Aggregation\Builder;

/**
* Filters the collection by range using numbers.
*
* @experimental
*
* @author Lee Siong Chan <[email protected]>
* @author Alan Poulain <[email protected]>
* @author Kai Dederichs <[email protected]>
*/
abstract class AbstractRangeFilter extends AbstractFilter implements RangeFilterInterface
{
abstract protected function normalizeValue(string $value, string $operator);

abstract protected function normalizeBetweenValues(array $values): ?array;

abstract protected function normalizeValues(array $values, string $property): ?array;

/**
* {@inheritdoc}
*/
protected function filterProperty(string $property, $values, Builder $aggregationBuilder, string $resourceClass, string $operationName = null, array &$context = [])
{
if (
!\is_array($values) ||
!$this->isPropertyEnabled($property, $resourceClass) ||
!$this->isPropertyMapped($property, $resourceClass)
) {
return;
}

$values = $this->normalizeValues($values, $property);
if (null === $values) {
return;
}

$matchField = $field = $property;

if ($this->isPropertyNested($property, $resourceClass)) {
[$matchField] = $this->addLookupsForNestedProperty($property, $aggregationBuilder, $resourceClass);
}

foreach ($values as $operator => $value) {
$this->addMatch(
$aggregationBuilder,
$field,
$matchField,
$operator,
$value
);
}
}

/**
* Adds the match stage according to the operator.
*/
protected function addMatch(Builder $aggregationBuilder, string $field, string $matchField, string $operator, string $value)
{
switch ($operator) {
case self::PARAMETER_BETWEEN:
$rangeValue = explode('..', $value);

$rangeValue = $this->normalizeBetweenValues($rangeValue);
if (null === $rangeValue) {
return;
}

if ($rangeValue[0] === $rangeValue[1]) {
$aggregationBuilder->match()->field($matchField)->equals($rangeValue[0]);

return;
}

$aggregationBuilder->match()->field($matchField)->gte($rangeValue[0])->lte($rangeValue[1]);

break;
case self::PARAMETER_GREATER_THAN:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$aggregationBuilder->match()->field($matchField)->gt($value);

break;
case self::PARAMETER_GREATER_THAN_OR_EQUAL:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$aggregationBuilder->match()->field($matchField)->gte($value);

break;
case self::PARAMETER_LESS_THAN:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$aggregationBuilder->match()->field($matchField)->lt($value);

break;
case self::PARAMETER_LESS_THAN_OR_EQUAL:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$aggregationBuilder->match()->field($matchField)->lte($value);

break;
}
}
}
104 changes: 3 additions & 101 deletions src/Doctrine/Odm/Filter/RangeFilter.php
Original file line number Diff line number Diff line change
@@ -13,116 +13,18 @@

namespace ApiPlatform\Doctrine\Odm\Filter;

use ApiPlatform\Doctrine\Common\Filter\RangeFilterInterface;
use ApiPlatform\Doctrine\Common\Filter\RangeFilterTrait;
use Doctrine\ODM\MongoDB\Aggregation\Builder;

/**
* Filters the collection by range.
* Filters the collection by range using numbers.
*
* @experimental
*
* @author Lee Siong Chan <[email protected]>
* @author Alan Poulain <[email protected]>
* @author Kai Dederichs <[email protected]>
*/
final class RangeFilter extends AbstractFilter implements RangeFilterInterface
final class RangeFilter extends AbstractRangeFilter
{
use RangeFilterTrait;

/**
* {@inheritdoc}
*/
protected function filterProperty(string $property, $values, Builder $aggregationBuilder, string $resourceClass, string $operationName = null, array &$context = [])
{
if (
!\is_array($values) ||
!$this->isPropertyEnabled($property, $resourceClass) ||
!$this->isPropertyMapped($property, $resourceClass)
) {
return;
}

$values = $this->normalizeValues($values, $property);
if (null === $values) {
return;
}

$matchField = $field = $property;

if ($this->isPropertyNested($property, $resourceClass)) {
[$matchField] = $this->addLookupsForNestedProperty($property, $aggregationBuilder, $resourceClass);
}

foreach ($values as $operator => $value) {
$this->addMatch(
$aggregationBuilder,
$field,
$matchField,
$operator,
$value
);
}
}

/**
* Adds the match stage according to the operator.
*/
protected function addMatch(Builder $aggregationBuilder, string $field, string $matchField, string $operator, string $value)
{
switch ($operator) {
case self::PARAMETER_BETWEEN:
$rangeValue = explode('..', $value);

$rangeValue = $this->normalizeBetweenValues($rangeValue);
if (null === $rangeValue) {
return;
}

if ($rangeValue[0] === $rangeValue[1]) {
$aggregationBuilder->match()->field($matchField)->equals($rangeValue[0]);

return;
}

$aggregationBuilder->match()->field($matchField)->gte($rangeValue[0])->lte($rangeValue[1]);

break;
case self::PARAMETER_GREATER_THAN:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$aggregationBuilder->match()->field($matchField)->gt($value);

break;
case self::PARAMETER_GREATER_THAN_OR_EQUAL:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$aggregationBuilder->match()->field($matchField)->gte($value);

break;
case self::PARAMETER_LESS_THAN:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$aggregationBuilder->match()->field($matchField)->lt($value);

break;
case self::PARAMETER_LESS_THAN_OR_EQUAL:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$aggregationBuilder->match()->field($matchField)->lte($value);

break;
}
}
}
29 changes: 29 additions & 0 deletions src/Doctrine/Odm/Filter/UuidRangeFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?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\UuidRangeFilterTrait;
use ApiPlatform\Doctrine\Orm\Filter\AbstractRangeFilter;

/**
* Filters the collection by range using UUIDs (UUID v6).
*
* @experimental
*
* @author Kai Dederichs <[email protected]>
*/
final class UuidRangeFilter extends AbstractRangeFilter
{
use UuidRangeFilterTrait;
}
152 changes: 152 additions & 0 deletions src/Doctrine/Orm/Filter/AbstractRangeFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?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\RangeFilterInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;

/**
* Base for different filter implementations.
*
* @author Lee Siong Chan <ahlee2326@me.com>
* @author Kai Dederichs <kai.dederichs@protonmail.com>
*/
abstract class AbstractRangeFilter extends AbstractContextAwareFilter implements RangeFilterInterface
{
abstract protected function normalizeValue(string $value, string $operator);

abstract protected function normalizeBetweenValues(array $values): ?array;

abstract protected function normalizeValues(array $values, string $property): ?array;

/**
* {@inheritdoc}
*/
protected function filterProperty(string $property, $values, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
if (
!\is_array($values) ||
!$this->isPropertyEnabled($property, $resourceClass) ||
!$this->isPropertyMapped($property, $resourceClass)
) {
return;
}

$values = $this->normalizeValues($values, $property);
if (null === $values) {
return;
}

$alias = $queryBuilder->getRootAliases()[0];
$field = $property;

if ($this->isPropertyNested($property, $resourceClass)) {
[$alias, $field] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass);
}

foreach ($values as $operator => $value) {
$this->addWhere(
$queryBuilder,
$queryNameGenerator,
$alias,
$field,
$operator,
$value
);
}
}

/**
* Adds the where clause according to the operator.
*
* @param string $alias
* @param string $field
* @param string $operator
* @param string $value
*/
protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, $alias, $field, $operator, $value)
{
$valueParameter = $queryNameGenerator->generateParameterName($field);

switch ($operator) {
case self::PARAMETER_BETWEEN:
$rangeValue = explode('..', $value);

$rangeValue = $this->normalizeBetweenValues($rangeValue);
if (null === $rangeValue) {
return;
}

if ($rangeValue[0] === $rangeValue[1]) {
$queryBuilder
->andWhere(sprintf('%s.%s = :%s', $alias, $field, $valueParameter))
->setParameter($valueParameter, $rangeValue[0]);

return;
}

$queryBuilder
->andWhere(sprintf('%1$s.%2$s BETWEEN :%3$s_1 AND :%3$s_2', $alias, $field, $valueParameter))
->setParameter(sprintf('%s_1', $valueParameter), $rangeValue[0])
->setParameter(sprintf('%s_2', $valueParameter), $rangeValue[1]);

break;
case self::PARAMETER_GREATER_THAN:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$queryBuilder
->andWhere(sprintf('%s.%s > :%s', $alias, $field, $valueParameter))
->setParameter($valueParameter, $value);

break;
case self::PARAMETER_GREATER_THAN_OR_EQUAL:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$queryBuilder
->andWhere(sprintf('%s.%s >= :%s', $alias, $field, $valueParameter))
->setParameter($valueParameter, $value);

break;
case self::PARAMETER_LESS_THAN:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$queryBuilder
->andWhere(sprintf('%s.%s < :%s', $alias, $field, $valueParameter))
->setParameter($valueParameter, $value);

break;
case self::PARAMETER_LESS_THAN_OR_EQUAL:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$queryBuilder
->andWhere(sprintf('%s.%s <= :%s', $alias, $field, $valueParameter))
->setParameter($valueParameter, $value);

break;
}
}
}
127 changes: 3 additions & 124 deletions src/Doctrine/Orm/Filter/RangeFilter.php
Original file line number Diff line number Diff line change
@@ -13,138 +13,17 @@

namespace ApiPlatform\Doctrine\Orm\Filter;

use ApiPlatform\Doctrine\Common\Filter\RangeFilterInterface;
use ApiPlatform\Doctrine\Common\Filter\RangeFilterTrait;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;

/**
* Filters the collection by range.
* Filters the collection by range using numbers.
*
* @author Lee Siong Chan <ahlee2326@me.com>
* @author Kai Dederichs <kai.dederichs@protonmail.com>
*
* @final
*/
class RangeFilter extends AbstractContextAwareFilter implements RangeFilterInterface
class RangeFilter extends AbstractRangeFilter
{
use RangeFilterTrait;

/**
* {@inheritdoc}
*/
protected function filterProperty(string $property, $values, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
if (
!\is_array($values) ||
!$this->isPropertyEnabled($property, $resourceClass) ||
!$this->isPropertyMapped($property, $resourceClass)
) {
return;
}

$values = $this->normalizeValues($values, $property);
if (null === $values) {
return;
}

$alias = $queryBuilder->getRootAliases()[0];
$field = $property;

if ($this->isPropertyNested($property, $resourceClass)) {
[$alias, $field] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass);
}

foreach ($values as $operator => $value) {
$this->addWhere(
$queryBuilder,
$queryNameGenerator,
$alias,
$field,
$operator,
$value
);
}
}

/**
* Adds the where clause according to the operator.
*
* @param string $alias
* @param string $field
* @param string $operator
* @param string $value
*/
protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, $alias, $field, $operator, $value)
{
$valueParameter = $queryNameGenerator->generateParameterName($field);

switch ($operator) {
case self::PARAMETER_BETWEEN:
$rangeValue = explode('..', $value);

$rangeValue = $this->normalizeBetweenValues($rangeValue);
if (null === $rangeValue) {
return;
}

if ($rangeValue[0] === $rangeValue[1]) {
$queryBuilder
->andWhere(sprintf('%s.%s = :%s', $alias, $field, $valueParameter))
->setParameter($valueParameter, $rangeValue[0]);

return;
}

$queryBuilder
->andWhere(sprintf('%1$s.%2$s BETWEEN :%3$s_1 AND :%3$s_2', $alias, $field, $valueParameter))
->setParameter(sprintf('%s_1', $valueParameter), $rangeValue[0])
->setParameter(sprintf('%s_2', $valueParameter), $rangeValue[1]);

break;
case self::PARAMETER_GREATER_THAN:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$queryBuilder
->andWhere(sprintf('%s.%s > :%s', $alias, $field, $valueParameter))
->setParameter($valueParameter, $value);

break;
case self::PARAMETER_GREATER_THAN_OR_EQUAL:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$queryBuilder
->andWhere(sprintf('%s.%s >= :%s', $alias, $field, $valueParameter))
->setParameter($valueParameter, $value);

break;
case self::PARAMETER_LESS_THAN:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$queryBuilder
->andWhere(sprintf('%s.%s < :%s', $alias, $field, $valueParameter))
->setParameter($valueParameter, $value);

break;
case self::PARAMETER_LESS_THAN_OR_EQUAL:
$value = $this->normalizeValue($value, $operator);
if (null === $value) {
return;
}

$queryBuilder
->andWhere(sprintf('%s.%s <= :%s', $alias, $field, $valueParameter))
->setParameter($valueParameter, $value);

break;
}
}
}
26 changes: 26 additions & 0 deletions src/Doctrine/Orm/Filter/UuidRangeFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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\UuidRangeFilterTrait;

/**
* Filters the collection by range using UUIDs.
*
* @author Kai Dederichs <kai.dederichs@protonmail.com>
*/
final class UuidRangeFilter extends AbstractRangeFilter
{
use UuidRangeFilterTrait;
}