From 29897975d58cbbdcb464083656d7b6789087f868 Mon Sep 17 00:00:00 2001 From: Kai Dederichs Date: Tue, 22 Mar 2022 10:48:35 +0100 Subject: [PATCH 01/11] Refactor RangeFilter to introduce a UuidRangeFilter --- .../Common/Filter/RangeFilterTrait.php | 8 +- .../Common/Filter/UuidRangeFilterTrait.php | 71 ++++++++ .../Odm/Filter/AbstractRangeFilter.php | 132 +++++++++++++++ src/Doctrine/Odm/Filter/RangeFilter.php | 104 +----------- src/Doctrine/Odm/Filter/UuidRangeFilter.php | 29 ++++ .../Orm/Filter/AbstractRangeFilter.php | 152 ++++++++++++++++++ src/Doctrine/Orm/Filter/RangeFilter.php | 127 +-------------- src/Doctrine/Orm/Filter/UuidRangeFilter.php | 26 +++ 8 files changed, 420 insertions(+), 229 deletions(-) create mode 100644 src/Doctrine/Common/Filter/UuidRangeFilterTrait.php create mode 100644 src/Doctrine/Odm/Filter/AbstractRangeFilter.php create mode 100644 src/Doctrine/Odm/Filter/UuidRangeFilter.php create mode 100644 src/Doctrine/Orm/Filter/AbstractRangeFilter.php create mode 100644 src/Doctrine/Orm/Filter/UuidRangeFilter.php diff --git a/src/Doctrine/Common/Filter/RangeFilterTrait.php b/src/Doctrine/Common/Filter/RangeFilterTrait.php index 6051ac48efd..7b601777ffb 100644 --- a/src/Doctrine/Common/Filter/RangeFilterTrait.php +++ b/src/Doctrine/Common/Filter/RangeFilterTrait.php @@ -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 * @author Alan Poulain @@ -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', [ diff --git a/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php b/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php new file mode 100644 index 00000000000..bad7614ceed --- /dev/null +++ b/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php @@ -0,0 +1,71 @@ + + * + * 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 + */ +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 ".."', 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); + } +} diff --git a/src/Doctrine/Odm/Filter/AbstractRangeFilter.php b/src/Doctrine/Odm/Filter/AbstractRangeFilter.php new file mode 100644 index 00000000000..54b44296626 --- /dev/null +++ b/src/Doctrine/Odm/Filter/AbstractRangeFilter.php @@ -0,0 +1,132 @@ + + * + * 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 + * @author Alan Poulain + * @author Kai Dederichs + */ +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; + } + } +} diff --git a/src/Doctrine/Odm/Filter/RangeFilter.php b/src/Doctrine/Odm/Filter/RangeFilter.php index b6c1c7d9d48..c036fddeafe 100644 --- a/src/Doctrine/Odm/Filter/RangeFilter.php +++ b/src/Doctrine/Odm/Filter/RangeFilter.php @@ -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 * @author Alan Poulain + * @author Kai Dederichs */ -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; - } - } } diff --git a/src/Doctrine/Odm/Filter/UuidRangeFilter.php b/src/Doctrine/Odm/Filter/UuidRangeFilter.php new file mode 100644 index 00000000000..dddfdbef922 --- /dev/null +++ b/src/Doctrine/Odm/Filter/UuidRangeFilter.php @@ -0,0 +1,29 @@ + + * + * 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 + */ +final class UuidRangeFilter extends AbstractRangeFilter +{ + use UuidRangeFilterTrait; +} diff --git a/src/Doctrine/Orm/Filter/AbstractRangeFilter.php b/src/Doctrine/Orm/Filter/AbstractRangeFilter.php new file mode 100644 index 00000000000..f4d43d4a0db --- /dev/null +++ b/src/Doctrine/Orm/Filter/AbstractRangeFilter.php @@ -0,0 +1,152 @@ + + * + * 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 + * @author Kai Dederichs + */ +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; + } + } +} diff --git a/src/Doctrine/Orm/Filter/RangeFilter.php b/src/Doctrine/Orm/Filter/RangeFilter.php index e8738bb00bd..fe2edafd9cf 100644 --- a/src/Doctrine/Orm/Filter/RangeFilter.php +++ b/src/Doctrine/Orm/Filter/RangeFilter.php @@ -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 + * @author Kai Dederichs * * @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; - } - } } diff --git a/src/Doctrine/Orm/Filter/UuidRangeFilter.php b/src/Doctrine/Orm/Filter/UuidRangeFilter.php new file mode 100644 index 00000000000..36dd02024de --- /dev/null +++ b/src/Doctrine/Orm/Filter/UuidRangeFilter.php @@ -0,0 +1,26 @@ + + * + * 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 + */ +final class UuidRangeFilter extends AbstractRangeFilter +{ + use UuidRangeFilterTrait; +} From 43db462fddda549d75b5a490d7f2b0ff04e18b20 Mon Sep 17 00:00:00 2001 From: Kai Dederichs Date: Tue, 22 Mar 2022 12:46:26 +0100 Subject: [PATCH 02/11] Add tests --- .github/workflows/ci.yml | 10 +- features/hydra/collection.feature | 91 +++++++++ src/Doctrine/Odm/Filter/UuidRangeFilter.php | 1 - .../Resources/config/doctrine_mongodb_odm.xml | 7 + .../Bundle/Resources/config/doctrine_orm.xml | 8 + tests/Core/Behat/DoctrineContext.php | 31 ++++ .../Filter/UuidRangeFilterTestTrait.php | 136 ++++++++++++++ .../Odm/Filter/UuidRangeFilterTest.php | 173 ++++++++++++++++++ .../Orm/Filter/UuidRangeFilterTest.php | 111 +++++++++++ .../TestBundle/Document/DummyUuidV6.php | 54 ++++++ .../TestBundle/Entity/DummyUuidV6.php | 49 +++++ .../Fixtures/TestBundle/Entity/SoManyUids.php | 56 ++++++ tests/Fixtures/app/config/config_mongodb.yml | 4 + tests/Fixtures/app/config/config_test.yml | 5 + .../ApiPlatformExtensionTest.php | 1 + 15 files changed, 731 insertions(+), 6 deletions(-) create mode 100644 tests/Doctrine/Common/Filter/UuidRangeFilterTestTrait.php create mode 100644 tests/Doctrine/Odm/Filter/UuidRangeFilterTest.php create mode 100644 tests/Doctrine/Orm/Filter/UuidRangeFilterTest.php create mode 100644 tests/Fixtures/TestBundle/Document/DummyUuidV6.php create mode 100644 tests/Fixtures/TestBundle/Entity/DummyUuidV6.php create mode 100644 tests/Fixtures/TestBundle/Entity/SoManyUids.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ac38113a3a..baa3f2fd5b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -248,7 +248,7 @@ jobs: vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=default-coverage --no-interaction --tags='~@php8' else if [ "${{ matrix.php }}" = '7.1' ]; then - vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=default --no-interaction --tags='~@symfony/uid&&~@php8' + vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=default --no-interaction --tags='~@symfony/uid&&~@php8&&~@uuid/v6' else vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=default --no-interaction --tags='~@php8' fi @@ -398,7 +398,7 @@ jobs: run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests # @TODO remove the tag "@symfony/uid" in 3.0 - run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction --tags='~@symfony/uid&&~php8' + run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction --tags='~@symfony/uid&&~php8&&~@uuid/v6' postgresql: name: Behat (PHP ${{ matrix.php }}) (PostgreSQL) @@ -554,9 +554,9 @@ jobs: run: | mkdir -p build/logs/behat if [ "$COVERAGE" = '1' ]; then - vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=mongodb-coverage --no-interaction --tags='~@php8' + vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=mongodb-coverage --no-interaction --tags='~@php8&&~@uuid/v6' else - vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=mongodb --no-interaction --tags='~@php8' + vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=mongodb --no-interaction --tags='~@php8&&~@uuid/v6' fi - name: Merge code coverage reports run: | @@ -905,7 +905,7 @@ jobs: - name: Run Behat tests run: | mkdir -p build/logs/behat - vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=mongodb --no-interaction + vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=mongodb --no-interaction --tags='~@uuid/v6 - name: Upload test artifacts if: always() uses: actions/upload-artifact@v1 diff --git a/features/hydra/collection.feature b/features/hydra/collection.feature index aebca7165d7..377162289ed 100644 --- a/features/hydra/collection.feature +++ b/features/hydra/collection.feature @@ -514,6 +514,97 @@ Feature: Collections support } """ + @createSchema + Scenario: Cursor-based pagination with ranged items and set cursor + Given there are 10 of these so many objects + When I send a "GET" request to "/so_manies?order%5Bid%5D=desc&id%5Blt%5D=7" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/SoMany$"}, + "@id": {"pattern": "^/so_manies$"}, + "@type": {"pattern": "^hydra:Collection"}, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/so_manies\\?order%5Bid%5D=desc&id%5Blt%5D=7$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:previous": {"pattern": "^/so_manies\\?order%5Bid%5D=desc&id%5Bgt%5D=6$"}, + "hydra:next": {"pattern": "^/so_manies\\?order%5Bid%5D=desc&id%5Blt%5D=4$"} + }, + "additionalProperties": false + }, + "hydra:member": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@id": { + "oneOf": [ + {"pattern": "^/so_manies/6$"}, + {"pattern": "^/so_manies/5$"}, + {"pattern": "^/so_manies/4$"} + ] + } + } + }, + "minItems": 3 + } + } + } + """ + + @createSchema + @uuid/v6 + Scenario: Cursor-based pagination with ranged items on uids + Given there are 10 of these so many uid objects + When I send a "GET" request to "/so_many_uids?order%5Bid%5D=desc&id%5Blt%5D=1ec5c128-f3d4-62d0-b528-68fef707f0bd" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/SoManyUids"}, + "@id": {"pattern": "^/so_many_uids"}, + "@type": {"pattern": "^hydra:Collection"}, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/so_many_uids\\?order%5Bid%5D=desc&id%5Blt%5D=1ec5c128-f3d4-62d0-b528-68fef707f0bd$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:previous": {"pattern": "^/so_many_uids\\?order%5Bid%5D=desc&id%5Bgt%5D=1ec5c128-f3d4-61ae-bb3c-68fef707f0bd$"}, + "hydra:next": {"pattern": "^/so_many_uids\\?order%5Bid%5D=desc&id%5Blt%5D=1ec5c128-f3d3-6fc4-8b52-68fef707f0bd$"} + }, + "additionalProperties": false + }, + "hydra:member": { + "type": "array", + "items": { + "type": "object", + "properties": { + "content": { + "oneOf": [ + {"pattern": "^Many #7$"}, + {"pattern": "^Many #6$"}, + {"pattern": "^Many #5$"} + ] + } + } + }, + "minItems": 3 + } + } + } + """ + @createSchema Scenario: Cursor-based pagination with range filtered items Given there are 10 of these so many objects diff --git a/src/Doctrine/Odm/Filter/UuidRangeFilter.php b/src/Doctrine/Odm/Filter/UuidRangeFilter.php index dddfdbef922..2f5576767c1 100644 --- a/src/Doctrine/Odm/Filter/UuidRangeFilter.php +++ b/src/Doctrine/Odm/Filter/UuidRangeFilter.php @@ -14,7 +14,6 @@ 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). diff --git a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml index 7e9d8872f17..7afb3491309 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml @@ -94,6 +94,13 @@ + + + + + + + diff --git a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml index edde68e28fb..0249bc469ab 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -62,6 +62,14 @@ + + + null + + + + + null diff --git a/tests/Core/Behat/DoctrineContext.php b/tests/Core/Behat/DoctrineContext.php index 8f8e1836fff..50f7c968640 100644 --- a/tests/Core/Behat/DoctrineContext.php +++ b/tests/Core/Behat/DoctrineContext.php @@ -163,6 +163,7 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SecuredDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Site; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SoMany; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SoManyUids; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SymfonyUuidDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Taxon; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ThirdLevel; @@ -177,6 +178,7 @@ use Doctrine\ORM\Tools\SchemaTool; use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ObjectManager; +use Ramsey\Uuid\Nonstandard\UuidV6; use Ramsey\Uuid\Uuid; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; @@ -293,6 +295,35 @@ public function thereAreOfTheseSoManyObjects(int $nb) $this->manager->flush(); } + /** + * @Given there are :nb of these so many uid objects + */ + public function thereAreOfTheseSoManyUidObjects(int $nb) + { + $ids = [ + '1ec5c128-f3d2-643a-8b17-68fef707f0bd', // 1 + '1ec5c128-f3d3-6cf4-a77e-68fef707f0bd', + '1ec5c128-f3d3-6e02-8834-68fef707f0bd', + '1ec5c128-f3d3-6ef2-b5f3-68fef707f0bd', + '1ec5c128-f3d3-6fc4-8b52-68fef707f0bd', + '1ec5c128-f3d4-6096-b820-68fef707f0bd', + '1ec5c128-f3d4-61ae-bb3c-68fef707f0bd', // 7 + '1ec5c128-f3d4-62d0-b528-68fef707f0bd', + '1ec5c128-f3d4-63f2-b845-68fef707f0bd', + '1ec5c128-f3d4-6514-8d2b-68fef707f0bd', // 10 + ]; + + for ($i = 1; $i <= $nb; ++$i) { + $ids[] = UuidV6::uuid6()->toString(); + $id = $ids[$i - 1] ?? null; + $dummy = new SoManyUids($id); + $dummy->content = 'Many #'.$i; + + $this->manager->persist($dummy); + } + $this->manager->flush(); + } + /** * @When some dummy table inheritance data but not api resource child are created */ diff --git a/tests/Doctrine/Common/Filter/UuidRangeFilterTestTrait.php b/tests/Doctrine/Common/Filter/UuidRangeFilterTestTrait.php new file mode 100644 index 00000000000..34607d03193 --- /dev/null +++ b/tests/Doctrine/Common/Filter/UuidRangeFilterTestTrait.php @@ -0,0 +1,136 @@ + + * + * 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\Doctrine\Common\Filter; + +trait UuidRangeFilterTestTrait +{ + private function provideApplyTestArguments(): array + { + return [ + 'between' => [ + null, + [ + 'id' => [ + 'between' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd..1ec5c128-f3d4-6514-8d2b-68fef707f0bd', + ], + ], + ], + 'between (same values)' => [ + null, + [ + 'id' => [ + 'between' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd..1ec5c128-f3d2-643a-8b17-68fef707f0bd', + ], + ], + ], + 'between (too many operands)' => [ + null, + [ + 'id' => [ + 'between' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd..1ec5c128-f3d4-6514-8d2b-68fef707f0bd..1ec5c128-f3d4-63f2-b845-68fef707f0bd', + ], + ], + ], + 'between (too few operands)' => [ + null, + [ + 'id' => [ + 'between' => '1ec5c128-f3d4-6514-8d2b-68fef707f0bd', + ], + ], + ], + 'between (non-uuid operands)' => [ + null, + [ + 'id' => [ + 'between' => 'abc..def', + ], + ], + ], + 'lt' => [ + null, + [ + 'id' => [ + 'lt' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd', + ], + ], + ], + 'lt (non-uuid)' => [ + null, + [ + 'id' => [ + 'lt' => '127.0.0.1', + ], + ], + ], + 'lte' => [ + null, + [ + 'id' => [ + 'lte' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd', + ], + ], + ], + 'lte (non-uuid)' => [ + null, + [ + 'id' => [ + 'lte' => '127.0.0.1', + ], + ], + ], + 'gt' => [ + null, + [ + 'id' => [ + 'gt' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd', + ], + ], + ], + 'gt (non-uuid)' => [ + null, + [ + 'id' => [ + 'gt' => '127.0.0.1', + ], + ], + ], + 'gte' => [ + null, + [ + 'id' => [ + 'gte' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd', + ], + ], + ], + 'gte (non-uuid)' => [ + null, + [ + 'id' => [ + 'gte' => '127.0.0.1', + ], + ], + ], + 'lte + gte' => [ + null, + [ + 'id' => [ + 'gte' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd', + 'lte' => '1ec5c128-f3d4-6514-8d2b-68fef707f0bd', + ], + ], + ], + ]; + } +} diff --git a/tests/Doctrine/Odm/Filter/UuidRangeFilterTest.php b/tests/Doctrine/Odm/Filter/UuidRangeFilterTest.php new file mode 100644 index 00000000000..1fefefd6de7 --- /dev/null +++ b/tests/Doctrine/Odm/Filter/UuidRangeFilterTest.php @@ -0,0 +1,173 @@ + + * + * 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\Doctrine\Odm\Filter; + +use ApiPlatform\Doctrine\Odm\Filter\UuidRangeFilter; +use ApiPlatform\Test\DoctrineMongoDbOdmFilterTestCase; +use ApiPlatform\Tests\Doctrine\Common\Filter\UuidRangeFilterTestTrait; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyUuidV6; + +class UuidRangeFilterTest extends DoctrineMongoDbOdmFilterTestCase +{ + use UuidRangeFilterTestTrait; + + protected $filterClass = UuidRangeFilter::class; + protected $resourceClass = DummyUuidV6::class; + + public function testGetDescriptionDefaultFields() + { + $filter = $this->buildFilter(); + + $this->assertEquals([ + 'id[between]' => [ + 'property' => 'id', + 'type' => 'string', + 'required' => false, + ], + 'id[gt]' => [ + 'property' => 'id', + 'type' => 'string', + 'required' => false, + ], + 'id[gte]' => [ + 'property' => 'id', + 'type' => 'string', + 'required' => false, + ], + 'id[lt]' => [ + 'property' => 'id', + 'type' => 'string', + 'required' => false, + ], + 'id[lte]' => [ + 'property' => 'id', + 'type' => 'string', + 'required' => false, + ], + ], $filter->getDescription($this->resourceClass)); + } + + public function provideApplyTestData(): array + { + return array_merge_recursive( + $this->provideApplyTestArguments(), + [ + 'between' => [ + [ + [ + '$match' => [ + '_id' => [ + '$gte' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd', + '$lte' => '1ec5c128-f3d4-6514-8d2b-68fef707f0bd', + ], + ], + ], + ], + ], + 'between (same values)' => [ + [ + [ + '$match' => [ + '_id' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd', + ], + ], + ], + ], + 'between (too many operands)' => [ + [], + ], + 'between (too few operands)' => [ + [], + ], + 'between (non-uuid operands)' => [ + [], + ], + 'lt' => [ + [ + [ + '$match' => [ + '_id' => [ + '$lt' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd', + ], + ], + ], + ], + ], + 'lt (non-uuid)' => [ + [], + ], + 'lte' => [ + [ + [ + '$match' => [ + '_id' => [ + '$lte' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd', + ], + ], + ], + ], + ], + 'lte (non-uuid)' => [ + [], + ], + 'gt' => [ + [ + [ + '$match' => [ + '_id' => [ + '$gt' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd', + ], + ], + ], + ], + ], + 'gt (non-uuid)' => [ + [], + ], + 'gte' => [ + [ + [ + '$match' => [ + '_id' => [ + '$gte' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd', + ], + ], + ], + ], + ], + 'gte (non-uuid)' => [ + [], + ], + 'lte + gte' => [ + [ + [ + '$match' => [ + '_id' => [ + '$gte' => '1ec5c128-f3d2-643a-8b17-68fef707f0bd', + ], + ], + ], + [ + '$match' => [ + '_id' => [ + '$lte' => '1ec5c128-f3d4-6514-8d2b-68fef707f0bd', + ], + ], + ], + ], + ], + ] + ); + } +} diff --git a/tests/Doctrine/Orm/Filter/UuidRangeFilterTest.php b/tests/Doctrine/Orm/Filter/UuidRangeFilterTest.php new file mode 100644 index 00000000000..ed594c78aae --- /dev/null +++ b/tests/Doctrine/Orm/Filter/UuidRangeFilterTest.php @@ -0,0 +1,111 @@ + + * + * 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\Doctrine\Orm\Filter; + +use ApiPlatform\Doctrine\Orm\Filter\UuidRangeFilter; +use ApiPlatform\Test\DoctrineOrmFilterTestCase; +use ApiPlatform\Tests\Doctrine\Common\Filter\UuidRangeFilterTestTrait; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyUuidV6; + +class UuidRangeFilterTest extends DoctrineOrmFilterTestCase +{ + use UuidRangeFilterTestTrait; + + protected $filterClass = UuidRangeFilter::class; + protected $resourceClass = DummyUuidV6::class; + + public function testGetDescriptionDefaultFields() + { + $filter = $this->buildFilter(); + $this->assertEquals([ + 'id[between]' => [ + 'property' => 'id', + 'type' => 'string', + 'required' => false, + ], + 'id[gt]' => [ + 'property' => 'id', + 'type' => 'string', + 'required' => false, + ], + 'id[gte]' => [ + 'property' => 'id', + 'type' => 'string', + 'required' => false, + ], + 'id[lt]' => [ + 'property' => 'id', + 'type' => 'string', + 'required' => false, + ], + 'id[lte]' => [ + 'property' => 'id', + 'type' => 'string', + 'required' => false, + ], + ], $filter->getDescription($this->resourceClass)); + } + + public function provideApplyTestData(): array + { + return array_merge_recursive( + $this->provideApplyTestArguments(), + [ + 'between' => [ + sprintf('SELECT o FROM %s o WHERE o.id BETWEEN :id_p1_1 AND :id_p1_2', Dummy::class), + ], + 'between (same values)' => [ + sprintf('SELECT o FROM %s o WHERE o.id = :id_p1', Dummy::class), + ], + 'between (too many operands)' => [ + sprintf('SELECT o FROM %s o', Dummy::class), + ], + 'between (too few operands)' => [ + sprintf('SELECT o FROM %s o', Dummy::class), + ], + 'between (non-uuid operands)' => [ + sprintf('SELECT o FROM %s o', Dummy::class), + ], + 'lt' => [ + sprintf('SELECT o FROM %s o WHERE o.id < :id_p1', Dummy::class), + ], + 'lt (non-uuid)' => [ + sprintf('SELECT o FROM %s o', Dummy::class), + ], + 'lte' => [ + sprintf('SELECT o FROM %s o WHERE o.id <= :id_p1', Dummy::class), + ], + 'lte (non-uuid)' => [ + sprintf('SELECT o FROM %s o', Dummy::class), + ], + 'gt' => [ + sprintf('SELECT o FROM %s o WHERE o.id > :id_p1', Dummy::class), + ], + 'gt (non-uuid)' => [ + sprintf('SELECT o FROM %s o', Dummy::class), + ], + 'gte' => [ + sprintf('SELECT o FROM %s o WHERE o.id >= :id_p1', Dummy::class), + ], + 'gte (non-uuid)' => [ + sprintf('SELECT o FROM %s o', Dummy::class), + ], + 'lte + gte' => [ + sprintf('SELECT o FROM %s o WHERE o.id >= :id_p1 AND o.id <= :id_p2', Dummy::class), + ], + ] + ); + } +} diff --git a/tests/Fixtures/TestBundle/Document/DummyUuidV6.php b/tests/Fixtures/TestBundle/Document/DummyUuidV6.php new file mode 100644 index 00000000000..3d7d5cabdc6 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/DummyUuidV6.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\Tests\Fixtures\TestBundle\Document; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Ramsey\Uuid\Nonstandard\UuidV6; + +/** + * @ApiResource(attributes={ + * "doctrine_mongodb"={ + * "execute_options"={ + * "allowDiskUse"=true + * } + * }, + * "filters"={ + * "my_dummy.mongodb.uuid_range", + * } + * }) + * @ODM\Document + */ +class DummyUuidV6 +{ + /** + * @var string|null The id + * + * @ODM\Id(strategy="NONE", type="string", nullable=true) + */ + private $id; + + public function __construct() + { + $this->id = UuidV6::uuid6(); + } + + /** + * @return string|null + */ + public function getId() + { + return $this->id->toString(); + } +} diff --git a/tests/Fixtures/TestBundle/Entity/DummyUuidV6.php b/tests/Fixtures/TestBundle/Entity/DummyUuidV6.php new file mode 100644 index 00000000000..e39e96d2caf --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyUuidV6.php @@ -0,0 +1,49 @@ + + * + * 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\Core\Annotation\ApiResource; +use Doctrine\ORM\Mapping as ORM; +use Ramsey\Uuid\Nonstandard\UuidV6; + +/** + * @ApiResource(attributes={ + * "filters"={ + * "my_dummy.uuid_range", + * } + * }) + * @ORM\Entity + */ +class DummyUuidV6 +{ + /** + * @ORM\Column(type="uuid", nullable=true) + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + public function __construct() + { + $this->id = UuidV6::uuid6(); + } + + /** + * @return \Ramsey\Uuid\UuidInterface + */ + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/SoManyUids.php b/tests/Fixtures/TestBundle/Entity/SoManyUids.php new file mode 100644 index 00000000000..52704d9497b --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/SoManyUids.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\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiFilter; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Doctrine\Orm\Filter\UuidRangeFilter; +use Doctrine\ORM\Mapping as ORM; +use Ramsey\Uuid\Nonstandard\UuidV6; + +/** + * @ORM\Entity + * @ApiResource(attributes={ + * "pagination_partial"=true, + * "pagination_via_cursor"={ + * {"field"="id", "direction"="DESC"} + * } + * }) + * + * @ApiFilter(UuidRangeFilter::class, properties={"id"}) + * @ApiFilter(OrderFilter::class, properties={"id"="DESC"}) + */ +class SoManyUids +{ + /** + * @ORM\Id + * @ORM\Column(type="uuid") + */ + public $id; + + /** + * @ORM\Column(nullable=true) + */ + public $content; + + public function __construct($id) + { + if ($id) { + $this->id = UuidV6::fromString($id); + } else { + $this->id = UuidV6::uuid6(); + } + } +} diff --git a/tests/Fixtures/app/config/config_mongodb.yml b/tests/Fixtures/app/config/config_mongodb.yml index 4640bdd1081..9e5828538fa 100644 --- a/tests/Fixtures/app/config/config_mongodb.yml +++ b/tests/Fixtures/app/config/config_mongodb.yml @@ -71,6 +71,10 @@ services: parent: 'api_platform.doctrine_mongodb.odm.search_filter' arguments: [ { 'name': 'ipartial', 'description': 'ipartial' } ] tags: [ { name: 'api_platform.filter', id: 'related_to_dummy_friend.mongodb.name' } ] + app.my_dummy_resource.mongodb.uuid_range_filter: + parent: 'api_platform.doctrine_mongodb.odm.uuid_range_filter' + arguments: [ { 'id': ~ } ] + tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.uuid_range' } ] ApiPlatform\Tests\Fixtures\TestBundle\DataProvider\ProductItemDataProvider: public: false diff --git a/tests/Fixtures/app/config/config_test.yml b/tests/Fixtures/app/config/config_test.yml index 019bbd7a2a5..dfe2a5717f5 100644 --- a/tests/Fixtures/app/config/config_test.yml +++ b/tests/Fixtures/app/config/config_test.yml @@ -75,6 +75,11 @@ services: parent: 'api_platform.doctrine.orm.search_filter' arguments: [ { 'name': 'ipartial', 'description': 'ipartial' } ] tags: [ { name: 'api_platform.filter', id: 'related_to_dummy_friend.name' } ] + + app.my_dummy_resource.uuid_range_filter: + parent: 'api_platform.doctrine.orm.uuid_range_filter' + arguments: [ { 'id': ~ } ] + tags: [ { name: 'api_platform.filter', id: 'my_dummy.uuid_range' } ] ApiPlatform\Tests\Fixtures\TestBundle\DataProvider\GeneratorDataProvider: public: false diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 3a85c9d296f..41c0d2d09ab 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -1105,6 +1105,7 @@ public function testDoctrineOrmConfiguration(): void 'api_platform.doctrine.orm.search_filter', 'api_platform.doctrine.orm.order_filter', 'api_platform.doctrine.orm.range_filter', + 'api_platform.doctrine.orm.uuid_range_filter', 'api_platform.doctrine.orm.query_extension.eager_loading', 'api_platform.doctrine.orm.query_extension.filter', 'api_platform.doctrine.orm.query_extension.filter_eager_loading', From 34cb56d5145aa7dec9a57df311b6d686b1c4e6a2 Mon Sep 17 00:00:00 2001 From: Kai Dederichs Date: Tue, 22 Mar 2022 12:51:44 +0100 Subject: [PATCH 03/11] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f34d8dc1b54..ec705d57508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ * Add support for `security_post_validation` attribute * Mark the GraphQL subsystem as stable (#4500) * feat(test): add `Client::loginUser()` +* Add `UuidRangeFilter` to allow cursor based pagination on UUIDs (V1 and V6) ## 2.6.8 From 0dbee77961a32cdb46627afa8cb082f6cd2107ea Mon Sep 17 00:00:00 2001 From: Kai Dederichs Date: Tue, 22 Mar 2022 13:16:18 +0100 Subject: [PATCH 04/11] Fix missing quote in CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index baa3f2fd5b4..32ea3b569c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -905,7 +905,7 @@ jobs: - name: Run Behat tests run: | mkdir -p build/logs/behat - vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=mongodb --no-interaction --tags='~@uuid/v6 + vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=mongodb --no-interaction --tags='~@uuid/v6' - name: Upload test artifacts if: always() uses: actions/upload-artifact@v1 From 789966ffb1507efd8c7bd4c36d540d46c79f8602 Mon Sep 17 00:00:00 2001 From: Kai Dederichs Date: Tue, 22 Mar 2022 13:21:47 +0100 Subject: [PATCH 05/11] Fix 7.1 return type mismatch --- src/Doctrine/Common/Filter/UuidRangeFilterTrait.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php b/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php index bad7614ceed..893c91ebfe2 100644 --- a/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php +++ b/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php @@ -50,8 +50,10 @@ protected function normalizeBetweenValues(array $values): ?array /** * Normalize the value. + * + * @return string|null */ - protected function normalizeValue(string $value, string $operator): ?string + protected function normalizeValue(string $value, string $operator) { if (!$this->isValidUid($value)) { $this->getLogger()->notice('Invalid filter ignored', [ From 5e79c82115c1e4d811eb4d0a063ca605c30065cd Mon Sep 17 00:00:00 2001 From: Kai Dederichs Date: Tue, 22 Mar 2022 13:44:02 +0100 Subject: [PATCH 06/11] add group to MongoDB tests so they are properly excluded --- tests/Doctrine/Odm/Filter/UuidRangeFilterTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Doctrine/Odm/Filter/UuidRangeFilterTest.php b/tests/Doctrine/Odm/Filter/UuidRangeFilterTest.php index 1fefefd6de7..d1f9e02f564 100644 --- a/tests/Doctrine/Odm/Filter/UuidRangeFilterTest.php +++ b/tests/Doctrine/Odm/Filter/UuidRangeFilterTest.php @@ -18,6 +18,11 @@ use ApiPlatform\Tests\Doctrine\Common\Filter\UuidRangeFilterTestTrait; use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyUuidV6; +/** + * @group mongodb + * + * @author Kai Dederichs + */ class UuidRangeFilterTest extends DoctrineMongoDbOdmFilterTestCase { use UuidRangeFilterTestTrait; From 4cdeb6a9e87cdab8abd39bbe913fd5e48a10f65e Mon Sep 17 00:00:00 2001 From: Kai Dederichs Date: Sat, 28 Oct 2023 17:32:19 +0200 Subject: [PATCH 07/11] chore: resolve changes between then and now and see if this still works --- features/hydra/collection.feature | 1 - .../Common/Filter/UuidRangeFilterTrait.php | 6 ++--- .../Odm/Filter/AbstractRangeFilter.php | 14 +++++++----- .../Orm/Filter/AbstractRangeFilter.php | 22 ++++++++----------- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/features/hydra/collection.feature b/features/hydra/collection.feature index 6c2d695ec57..a9cd7026ebd 100644 --- a/features/hydra/collection.feature +++ b/features/hydra/collection.feature @@ -600,7 +600,6 @@ Feature: Collections support """ @createSchema - @uuid/v6 Scenario: Cursor-based pagination with ranged items on uids Given there are 10 of these so many uid objects When I send a "GET" request to "/so_many_uids?order%5Bid%5D=desc&id%5Blt%5D=1ec5c128-f3d4-62d0-b528-68fef707f0bd" diff --git a/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php b/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php index 893c91ebfe2..6376b27b0d6 100644 --- a/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php +++ b/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php @@ -50,14 +50,12 @@ protected function normalizeBetweenValues(array $values): ?array /** * Normalize the value. - * - * @return string|null */ - protected function normalizeValue(string $value, string $operator) + protected function normalizeValue(string $value, string $operator): string | null { if (!$this->isValidUid($value)) { $this->getLogger()->notice('Invalid filter ignored', [ - 'exception' => new InvalidArgumentException(sprintf('Invalid value for "[%s]", expected number', $operator)), + 'exception' => new InvalidArgumentException(sprintf('Invalid value for "[%s]", expected uuid', $operator)), ]); return null; diff --git a/src/Doctrine/Odm/Filter/AbstractRangeFilter.php b/src/Doctrine/Odm/Filter/AbstractRangeFilter.php index 54b44296626..20e8bf02609 100644 --- a/src/Doctrine/Odm/Filter/AbstractRangeFilter.php +++ b/src/Doctrine/Odm/Filter/AbstractRangeFilter.php @@ -14,8 +14,10 @@ namespace ApiPlatform\Doctrine\Odm\Filter; use ApiPlatform\Doctrine\Common\Filter\RangeFilterInterface; +use ApiPlatform\Metadata\Operation; use Doctrine\ODM\MongoDB\Aggregation\Builder; + /** * Filters the collection by range using numbers. * @@ -36,12 +38,12 @@ abstract protected function normalizeValues(array $values, string $property): ?a /** * {@inheritdoc} */ - protected function filterProperty(string $property, $values, Builder $aggregationBuilder, string $resourceClass, string $operationName = null, array &$context = []) + protected function filterProperty(string $property, $values, Builder $aggregationBuilder, string $resourceClass, Operation $operation = null, array &$context = []): void { if ( - !\is_array($values) || - !$this->isPropertyEnabled($property, $resourceClass) || - !$this->isPropertyMapped($property, $resourceClass) + !\is_array($values) + || !$this->isPropertyEnabled($property, $resourceClass) + || !$this->isPropertyMapped($property, $resourceClass) ) { return; } @@ -71,11 +73,11 @@ protected function filterProperty(string $property, $values, Builder $aggregatio /** * Adds the match stage according to the operator. */ - protected function addMatch(Builder $aggregationBuilder, string $field, string $matchField, string $operator, string $value) + protected function addMatch(Builder $aggregationBuilder, string $field, string $matchField, string $operator, string $value): void { switch ($operator) { case self::PARAMETER_BETWEEN: - $rangeValue = explode('..', $value); + $rangeValue = explode('..', $value, 2); $rangeValue = $this->normalizeBetweenValues($rangeValue); if (null === $rangeValue) { diff --git a/src/Doctrine/Orm/Filter/AbstractRangeFilter.php b/src/Doctrine/Orm/Filter/AbstractRangeFilter.php index f4d43d4a0db..905e81491d5 100644 --- a/src/Doctrine/Orm/Filter/AbstractRangeFilter.php +++ b/src/Doctrine/Orm/Filter/AbstractRangeFilter.php @@ -15,6 +15,7 @@ use ApiPlatform\Doctrine\Common\Filter\RangeFilterInterface; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; /** @@ -23,7 +24,7 @@ * @author Lee Siong Chan * @author Kai Dederichs */ -abstract class AbstractRangeFilter extends AbstractContextAwareFilter implements RangeFilterInterface +abstract class AbstractRangeFilter extends AbstractFilter implements RangeFilterInterface { abstract protected function normalizeValue(string $value, string $operator); @@ -34,12 +35,12 @@ abstract protected function normalizeValues(array $values, string $property): ?a /** * {@inheritdoc} */ - protected function filterProperty(string $property, $values, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null) + protected function filterProperty(string $property, $values, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void { if ( - !\is_array($values) || - !$this->isPropertyEnabled($property, $resourceClass) || - !$this->isPropertyMapped($property, $resourceClass) + !\is_array($values) + || !$this->isPropertyEnabled($property, $resourceClass) + || !$this->isPropertyMapped($property, $resourceClass) ) { return; } @@ -53,7 +54,7 @@ protected function filterProperty(string $property, $values, QueryBuilder $query $field = $property; if ($this->isPropertyNested($property, $resourceClass)) { - [$alias, $field] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass); + [$alias, $field] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass, Join::INNER_JOIN); } foreach ($values as $operator => $value) { @@ -70,19 +71,14 @@ protected function filterProperty(string $property, $values, QueryBuilder $query /** * 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) + protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, string $operator, string $value): void { $valueParameter = $queryNameGenerator->generateParameterName($field); switch ($operator) { case self::PARAMETER_BETWEEN: - $rangeValue = explode('..', $value); + $rangeValue = explode('..', $value, 2); $rangeValue = $this->normalizeBetweenValues($rangeValue); if (null === $rangeValue) { From 2acfcf9fed16744cc085fa11395eb4ff97ea8457 Mon Sep 17 00:00:00 2001 From: Kai Dederichs Date: Sat, 28 Oct 2023 17:59:47 +0200 Subject: [PATCH 08/11] feat: fix phpunit tests --- .../Filter/UuidRangeFilterTestTrait.php | 2 +- .../Odm/Filter/UuidRangeFilterTest.php | 10 ++--- .../Orm/Filter/UuidRangeFilterTest.php | 11 ++--- .../TestBundle/Document/DummyUuidV6.php | 40 ++++++------------- .../TestBundle/Entity/DummyUuidV6.php | 36 +++++++---------- 5 files changed, 38 insertions(+), 61 deletions(-) diff --git a/tests/Doctrine/Common/Filter/UuidRangeFilterTestTrait.php b/tests/Doctrine/Common/Filter/UuidRangeFilterTestTrait.php index 34607d03193..26a8e958f0c 100644 --- a/tests/Doctrine/Common/Filter/UuidRangeFilterTestTrait.php +++ b/tests/Doctrine/Common/Filter/UuidRangeFilterTestTrait.php @@ -15,7 +15,7 @@ trait UuidRangeFilterTestTrait { - private function provideApplyTestArguments(): array + private static function provideApplyTestArguments(): array { return [ 'between' => [ diff --git a/tests/Doctrine/Odm/Filter/UuidRangeFilterTest.php b/tests/Doctrine/Odm/Filter/UuidRangeFilterTest.php index d1f9e02f564..f5837cc0077 100644 --- a/tests/Doctrine/Odm/Filter/UuidRangeFilterTest.php +++ b/tests/Doctrine/Odm/Filter/UuidRangeFilterTest.php @@ -27,10 +27,10 @@ class UuidRangeFilterTest extends DoctrineMongoDbOdmFilterTestCase { use UuidRangeFilterTestTrait; - protected $filterClass = UuidRangeFilter::class; - protected $resourceClass = DummyUuidV6::class; + protected string $filterClass = UuidRangeFilter::class; + protected string $resourceClass = DummyUuidV6::class; - public function testGetDescriptionDefaultFields() + public function testGetDescriptionDefaultFields(): void { $filter = $this->buildFilter(); @@ -63,10 +63,10 @@ public function testGetDescriptionDefaultFields() ], $filter->getDescription($this->resourceClass)); } - public function provideApplyTestData(): array + public static function provideApplyTestData(): array { return array_merge_recursive( - $this->provideApplyTestArguments(), + self::provideApplyTestArguments(), [ 'between' => [ [ diff --git a/tests/Doctrine/Orm/Filter/UuidRangeFilterTest.php b/tests/Doctrine/Orm/Filter/UuidRangeFilterTest.php index ed594c78aae..12b6e3f0a58 100644 --- a/tests/Doctrine/Orm/Filter/UuidRangeFilterTest.php +++ b/tests/Doctrine/Orm/Filter/UuidRangeFilterTest.php @@ -23,12 +23,13 @@ class UuidRangeFilterTest extends DoctrineOrmFilterTestCase { use UuidRangeFilterTestTrait; - protected $filterClass = UuidRangeFilter::class; - protected $resourceClass = DummyUuidV6::class; + protected string $filterClass = UuidRangeFilter::class; + protected string $resourceClass = DummyUuidV6::class; - public function testGetDescriptionDefaultFields() + public function testGetDescriptionDefaultFields(): void { $filter = $this->buildFilter(); + $this->assertEquals([ 'id[between]' => [ 'property' => 'id', @@ -58,10 +59,10 @@ public function testGetDescriptionDefaultFields() ], $filter->getDescription($this->resourceClass)); } - public function provideApplyTestData(): array + public static function provideApplyTestData(): array { return array_merge_recursive( - $this->provideApplyTestArguments(), + self::provideApplyTestArguments(), [ 'between' => [ sprintf('SELECT o FROM %s o WHERE o.id BETWEEN :id_p1_1 AND :id_p1_2', Dummy::class), diff --git a/tests/Fixtures/TestBundle/Document/DummyUuidV6.php b/tests/Fixtures/TestBundle/Document/DummyUuidV6.php index 3d7d5cabdc6..564069391b4 100644 --- a/tests/Fixtures/TestBundle/Document/DummyUuidV6.php +++ b/tests/Fixtures/TestBundle/Document/DummyUuidV6.php @@ -13,42 +13,26 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; -use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Metadata\ApiResource; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; -use Ramsey\Uuid\Nonstandard\UuidV6; - -/** - * @ApiResource(attributes={ - * "doctrine_mongodb"={ - * "execute_options"={ - * "allowDiskUse"=true - * } - * }, - * "filters"={ - * "my_dummy.mongodb.uuid_range", - * } - * }) - * @ODM\Document - */ +use Symfony\Component\Uid\Uuid; +use Symfony\Component\Uid\UuidV6; + + +#[ApiResource(extraProperties: ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]]], filters: ['my_dummy.mongodb.uuid_range'])] +#[ODM\Document] class DummyUuidV6 { - /** - * @var string|null The id - * - * @ODM\Id(strategy="NONE", type="string", nullable=true) - */ - private $id; + #[ODM\Id(strategy: 'NONE', type: 'string', nullable: true)] + private ?Uuid $id = null; public function __construct() { - $this->id = UuidV6::uuid6(); + $this->id = UuidV6::v6(); } - /** - * @return string|null - */ - public function getId() + public function getId(): ?Uuid { - return $this->id->toString(); + return $this->id; } } diff --git a/tests/Fixtures/TestBundle/Entity/DummyUuidV6.php b/tests/Fixtures/TestBundle/Entity/DummyUuidV6.php index e39e96d2caf..2b7334654ba 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyUuidV6.php +++ b/tests/Fixtures/TestBundle/Entity/DummyUuidV6.php @@ -13,36 +13,28 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; -use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Metadata\ApiResource; use Doctrine\ORM\Mapping as ORM; -use Ramsey\Uuid\Nonstandard\UuidV6; - -/** - * @ApiResource(attributes={ - * "filters"={ - * "my_dummy.uuid_range", - * } - * }) - * @ORM\Entity - */ +use Symfony\Bridge\Doctrine\Types\UuidType; +use Symfony\Component\Uid\Uuid; +use Symfony\Component\Uid\UuidV6; + +#[ApiResource(filters: ['my_dummy.uuid_range'])] +#[ORM\Entity] class DummyUuidV6 { - /** - * @ORM\Column(type="uuid", nullable=true) - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - */ - private $id; + + #[ORM\Column(type: UuidType::NAME)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private Uuid $id; public function __construct() { - $this->id = UuidV6::uuid6(); + $this->id = UuidV6::v6(); } - /** - * @return \Ramsey\Uuid\UuidInterface - */ - public function getId() + public function getId(): Uuid { return $this->id; } From cf1219ccdf3d2943eda6e22947dab8cb7e15c884 Mon Sep 17 00:00:00 2001 From: Kai Dederichs Date: Sat, 28 Oct 2023 18:11:19 +0200 Subject: [PATCH 09/11] feat: Probably fix behat tests --- tests/Behat/DoctrineContext.php | 6 +-- .../Fixtures/TestBundle/Entity/SoManyUids.php | 43 ++++++++----------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index fd1632b713c..3be61ce9ada 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -205,9 +205,9 @@ use Doctrine\ORM\Tools\SchemaTool; use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ObjectManager; -use Ramsey\Uuid\Nonstandard\UuidV6; use Ramsey\Uuid\Uuid; use Symfony\Component\Uid\Uuid as SymfonyUuid; +use Symfony\Component\Uid\UuidV6; /** * Defines application features from the specific context. @@ -326,7 +326,7 @@ public function thereAreOfTheseSoManyObjects(int $nb): void /** * @Given there are :nb of these so many uid objects */ - public function thereAreOfTheseSoManyUidObjects(int $nb) + public function thereAreOfTheseSoManyUidObjects(int $nb): void { $ids = [ '1ec5c128-f3d2-643a-8b17-68fef707f0bd', // 1 @@ -342,7 +342,7 @@ public function thereAreOfTheseSoManyUidObjects(int $nb) ]; for ($i = 1; $i <= $nb; ++$i) { - $ids[] = UuidV6::uuid6()->toString(); + $ids[] = UuidV6::v6()->toRfc4122(); $id = $ids[$i - 1] ?? null; $dummy = new SoManyUids($id); $dummy->content = 'Many #'.$i; diff --git a/tests/Fixtures/TestBundle/Entity/SoManyUids.php b/tests/Fixtures/TestBundle/Entity/SoManyUids.php index 52704d9497b..a3a8826c0c8 100644 --- a/tests/Fixtures/TestBundle/Entity/SoManyUids.php +++ b/tests/Fixtures/TestBundle/Entity/SoManyUids.php @@ -13,36 +13,31 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; -use ApiPlatform\Core\Annotation\ApiFilter; -use ApiPlatform\Core\Annotation\ApiResource; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Doctrine\Orm\Filter\UuidRangeFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; use Doctrine\ORM\Mapping as ORM; -use Ramsey\Uuid\Nonstandard\UuidV6; +use Symfony\Bridge\Doctrine\Types\UuidType; +use Symfony\Component\Uid\Uuid; +use Symfony\Component\Uid\UuidV6; -/** - * @ORM\Entity - * @ApiResource(attributes={ - * "pagination_partial"=true, - * "pagination_via_cursor"={ - * {"field"="id", "direction"="DESC"} - * } - * }) - * - * @ApiFilter(UuidRangeFilter::class, properties={"id"}) - * @ApiFilter(OrderFilter::class, properties={"id"="DESC"}) - */ +#[ORM\Entity] +#[ApiResource( + paginationViaCursor: [ + ['field' => 'id', 'direction' => 'DESC'] + ], + paginationPartial: true +)] +#[ApiFilter(UuidRangeFilter::class, properties: ['id'])] +#[ApiFilter(OrderFilter::class, properties: ["id" => "DESC"])] class SoManyUids { - /** - * @ORM\Id - * @ORM\Column(type="uuid") - */ - public $id; + #[ORM\Id] + #[ORM\Column(type: UuidType::NAME)] + public Uuid $id; - /** - * @ORM\Column(nullable=true) - */ + #[ORM\Column(nullable: true)] public $content; public function __construct($id) @@ -50,7 +45,7 @@ public function __construct($id) if ($id) { $this->id = UuidV6::fromString($id); } else { - $this->id = UuidV6::uuid6(); + $this->id = UuidV6::v6(); } } } From 821650ae3b3d2382b4cc4b0b2d24b8ed304fc162 Mon Sep 17 00:00:00 2001 From: Kai Dederichs Date: Sat, 28 Oct 2023 19:32:13 +0200 Subject: [PATCH 10/11] feat: Switch to v7, fix route name --- features/hydra/collection.feature | 12 ++++----- .../Common/Filter/UuidRangeFilterTrait.php | 10 +++---- tests/Behat/DoctrineContext.php | 27 +++++++++---------- .../TestBundle/Entity/DummyUuidV6.php | 6 ++--- .../Entity/{SoManyUids.php => SoManyUid.php} | 10 +++---- 5 files changed, 28 insertions(+), 37 deletions(-) rename tests/Fixtures/TestBundle/Entity/{SoManyUids.php => SoManyUid.php} (82%) diff --git a/features/hydra/collection.feature b/features/hydra/collection.feature index a9cd7026ebd..684dca7336b 100644 --- a/features/hydra/collection.feature +++ b/features/hydra/collection.feature @@ -602,7 +602,7 @@ Feature: Collections support @createSchema Scenario: Cursor-based pagination with ranged items on uids Given there are 10 of these so many uid objects - When I send a "GET" request to "/so_many_uids?order%5Bid%5D=desc&id%5Blt%5D=1ec5c128-f3d4-62d0-b528-68fef707f0bd" + When I send a "GET" request to "/so_many_uids?order%5Bid%5D=desc&id%5Blt%5D=018b7743-c432-76ad-bb16-66151bb60a8a" Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" @@ -611,16 +611,16 @@ Feature: Collections support { "type": "object", "properties": { - "@context": {"pattern": "^/contexts/SoManyUids"}, - "@id": {"pattern": "^/so_many_uids"}, + "@context": {"pattern": "^/contexts/SoManyUid"}, + "@id": {"pattern": "^/so_many_uid"}, "@type": {"pattern": "^hydra:Collection"}, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/so_many_uids\\?order%5Bid%5D=desc&id%5Blt%5D=1ec5c128-f3d4-62d0-b528-68fef707f0bd$"}, + "@id": {"pattern": "^/so_many_uids\\?order%5Bid%5D=desc&id%5Blt%5D=018b7743-c432-76ad-bb16-66151bb60a8a$"}, "@type": {"pattern": "^hydra:PartialCollectionView$"}, - "hydra:previous": {"pattern": "^/so_many_uids\\?order%5Bid%5D=desc&id%5Bgt%5D=1ec5c128-f3d4-61ae-bb3c-68fef707f0bd$"}, - "hydra:next": {"pattern": "^/so_many_uids\\?order%5Bid%5D=desc&id%5Blt%5D=1ec5c128-f3d3-6fc4-8b52-68fef707f0bd$"} + "hydra:previous": {"pattern": "^/so_many_uids\\?order%5Bid%5D=desc&id%5Bgt%5D=018b7743-a62c-7be8-8e4c-67c363fd5a01$"}, + "hydra:next": {"pattern": "^/so_many_uids\\?order%5Bid%5D=desc&id%5Blt%5D=018b7743-6a9f-72af-9587-c7e06f11c33e$"} }, "additionalProperties": false }, diff --git a/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php b/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php index 6376b27b0d6..13814ef401a 100644 --- a/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php +++ b/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Doctrine\Common\Filter; use ApiPlatform\Exception\InvalidArgumentException; +use Symfony\Component\Uid\Uuid; /** * Trait for filtering the collection by range using UUIDs (UUID v6). @@ -37,7 +38,7 @@ protected function normalizeBetweenValues(array $values): ?array return null; } - if (!$this->isValidUid($values[0]) || !$this->isValidUid($values[1])) { + if (!Uuid::isValid($values[0]) || !Uuid::isValid($values[1])) { $this->getLogger()->notice('Invalid filter ignored', [ 'exception' => new InvalidArgumentException(sprintf('Invalid values for "[%s]" range, expected uuids', self::PARAMETER_BETWEEN)), ]); @@ -53,7 +54,7 @@ protected function normalizeBetweenValues(array $values): ?array */ protected function normalizeValue(string $value, string $operator): string | null { - if (!$this->isValidUid($value)) { + if (!Uuid::isValid($value)) { $this->getLogger()->notice('Invalid filter ignored', [ 'exception' => new InvalidArgumentException(sprintf('Invalid value for "[%s]", expected uuid', $operator)), ]); @@ -63,9 +64,4 @@ protected function normalizeValue(string $value, string $operator): string | nul 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); - } } diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index 3be61ce9ada..eaafe5ca0b1 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -184,7 +184,7 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SeparatedEntity; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Site; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SoMany; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SoManyUids; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SoManyUid; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SymfonyUuidDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Taxon; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ThirdLevel; @@ -207,7 +207,6 @@ use Doctrine\Persistence\ObjectManager; use Ramsey\Uuid\Uuid; use Symfony\Component\Uid\Uuid as SymfonyUuid; -use Symfony\Component\Uid\UuidV6; /** * Defines application features from the specific context. @@ -329,22 +328,22 @@ public function thereAreOfTheseSoManyObjects(int $nb): void public function thereAreOfTheseSoManyUidObjects(int $nb): void { $ids = [ - '1ec5c128-f3d2-643a-8b17-68fef707f0bd', // 1 - '1ec5c128-f3d3-6cf4-a77e-68fef707f0bd', - '1ec5c128-f3d3-6e02-8834-68fef707f0bd', - '1ec5c128-f3d3-6ef2-b5f3-68fef707f0bd', - '1ec5c128-f3d3-6fc4-8b52-68fef707f0bd', - '1ec5c128-f3d4-6096-b820-68fef707f0bd', - '1ec5c128-f3d4-61ae-bb3c-68fef707f0bd', // 7 - '1ec5c128-f3d4-62d0-b528-68fef707f0bd', - '1ec5c128-f3d4-63f2-b845-68fef707f0bd', - '1ec5c128-f3d4-6514-8d2b-68fef707f0bd', // 10 + '018b7741-df00-7d0b-9895-bcc1fa36daad', // 1 + '018b7743-0bff-7298-9916-11e31114d2f4', + '018b7743-3122-7337-a658-f3fddb4b2764', + '018b7743-4e2f-7126-ac8c-befb62055095', + '018b7743-6a9f-72af-9587-c7e06f11c33e', + '018b7743-8758-7f01-a3fd-3e63af5eaa5f', + '018b7743-a62c-7be8-8e4c-67c363fd5a01', // 7 + '018b7743-c432-76ad-bb16-66151bb60a8a', + '018b7743-dfb1-708f-a76c-b645e49fc231', + '018b7743-f975-77da-8289-f25d475def71', // 10 ]; for ($i = 1; $i <= $nb; ++$i) { - $ids[] = UuidV6::v6()->toRfc4122(); + //$ids[] = UuidV7::v7()->toRfc4122(); $id = $ids[$i - 1] ?? null; - $dummy = new SoManyUids($id); + $dummy = new SoManyUid($id); $dummy->content = 'Many #'.$i; $this->manager->persist($dummy); diff --git a/tests/Fixtures/TestBundle/Entity/DummyUuidV6.php b/tests/Fixtures/TestBundle/Entity/DummyUuidV6.php index 2b7334654ba..6115a318579 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyUuidV6.php +++ b/tests/Fixtures/TestBundle/Entity/DummyUuidV6.php @@ -15,23 +15,21 @@ use ApiPlatform\Metadata\ApiResource; use Doctrine\ORM\Mapping as ORM; -use Symfony\Bridge\Doctrine\Types\UuidType; use Symfony\Component\Uid\Uuid; -use Symfony\Component\Uid\UuidV6; #[ApiResource(filters: ['my_dummy.uuid_range'])] #[ORM\Entity] class DummyUuidV6 { - #[ORM\Column(type: UuidType::NAME)] + #[ORM\Column(type: 'symfony_uuid')] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'AUTO')] private Uuid $id; public function __construct() { - $this->id = UuidV6::v6(); + $this->id = Uuid::v7(); } public function getId(): Uuid diff --git a/tests/Fixtures/TestBundle/Entity/SoManyUids.php b/tests/Fixtures/TestBundle/Entity/SoManyUid.php similarity index 82% rename from tests/Fixtures/TestBundle/Entity/SoManyUids.php rename to tests/Fixtures/TestBundle/Entity/SoManyUid.php index a3a8826c0c8..ce49967dc7c 100644 --- a/tests/Fixtures/TestBundle/Entity/SoManyUids.php +++ b/tests/Fixtures/TestBundle/Entity/SoManyUid.php @@ -18,9 +18,7 @@ use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiResource; use Doctrine\ORM\Mapping as ORM; -use Symfony\Bridge\Doctrine\Types\UuidType; use Symfony\Component\Uid\Uuid; -use Symfony\Component\Uid\UuidV6; #[ORM\Entity] #[ApiResource( @@ -31,10 +29,10 @@ )] #[ApiFilter(UuidRangeFilter::class, properties: ['id'])] #[ApiFilter(OrderFilter::class, properties: ["id" => "DESC"])] -class SoManyUids +class SoManyUid { #[ORM\Id] - #[ORM\Column(type: UuidType::NAME)] + #[ORM\Column(type: 'symfony_uuid')] public Uuid $id; #[ORM\Column(nullable: true)] @@ -43,9 +41,9 @@ class SoManyUids public function __construct($id) { if ($id) { - $this->id = UuidV6::fromString($id); + $this->id = Uuid::fromString($id); } else { - $this->id = UuidV6::v6(); + $this->id = Uuid::v7(); } } } From 91a0b968cf9c44f9936c3e59635e8649ea5cab8a Mon Sep 17 00:00:00 2001 From: Kai Dederichs Date: Sat, 28 Oct 2023 20:01:30 +0200 Subject: [PATCH 11/11] feat: Add SoManyUid document --- features/hydra/collection.feature | 1 + src/Doctrine/Common/Filter/UuidRangeFilterTrait.php | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/features/hydra/collection.feature b/features/hydra/collection.feature index 684dca7336b..3e52a82ba5d 100644 --- a/features/hydra/collection.feature +++ b/features/hydra/collection.feature @@ -604,6 +604,7 @@ Feature: Collections support Given there are 10 of these so many uid objects When I send a "GET" request to "/so_many_uids?order%5Bid%5D=desc&id%5Blt%5D=018b7743-c432-76ad-bb16-66151bb60a8a" Then the response status code should be 200 + Then print last JSON response And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" And the JSON should be valid according to this schema: diff --git a/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php b/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php index 13814ef401a..2f527de6982 100644 --- a/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php +++ b/src/Doctrine/Common/Filter/UuidRangeFilterTrait.php @@ -46,13 +46,13 @@ protected function normalizeBetweenValues(array $values): ?array return null; } - return [$values[0], $values[1]]; + return [Uuid::fromString($values[0]), Uuid::fromString($values[1])]; } /** * Normalize the value. */ - protected function normalizeValue(string $value, string $operator): string | null + protected function normalizeValue(string $value, string $operator): Uuid | null { if (!Uuid::isValid($value)) { $this->getLogger()->notice('Invalid filter ignored', [ @@ -62,6 +62,6 @@ protected function normalizeValue(string $value, string $operator): string | nul return null; } - return $value; + return Uuid::fromString($value); } }