diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 666a70be188..79b7a82058e 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -265,6 +265,7 @@ public function thereAreDummyObjectsWithRelatedDummy(int $nb) /** * @Given there are :nb dummy objects with relatedDummy and its thirdLevel + * @Given there is :nb dummy object with relatedDummy and its thirdLevel */ public function thereAreDummyObjectsWithRelatedDummyAndItsThirdLevel(int $nb) { @@ -334,6 +335,7 @@ public function thereAreDummyObjectsWithRelatedDummies(int $nb, int $nbrelated) /** * @Given there are :nb dummy objects with dummyDate + * @Given there is :nb dummy object with dummyDate */ public function thereAreDummyObjectsWithDummyDate(int $nb) { @@ -471,6 +473,7 @@ public function thereAreDummyObjectsWithDummyPrice(int $nb) /** * @Given there are :nb dummy objects with dummyBoolean :bool + * @Given there is :nb dummy object with dummyBoolean :bool */ public function thereAreDummyObjectsWithDummyBoolean(int $nb, string $bool) { @@ -825,6 +828,7 @@ public function createPeopleWithPets() /** * @Given there are :nb dummydate objects with dummyDate + * @Given there is :nb dummydate object with dummyDate */ public function thereAreDummyDateObjectsWithDummyDate(int $nb) { diff --git a/features/graphql/filters.feature b/features/graphql/filters.feature new file mode 100644 index 00000000000..f1c68392ea2 --- /dev/null +++ b/features/graphql/filters.feature @@ -0,0 +1,94 @@ +Feature: Collections filtering + In order to retrieve subsets of collections + As an API consumer + I need to be able to set filters + + @createSchema + @dropSchema + Scenario: Retrieve a collection filtered using the boolean filter + Given there is 1 dummy object with dummyBoolean true + And there is 1 dummy object with dummyBoolean false + When I send the following GraphQL request: + """ + { + dummies(dummyBoolean: false) { + edges { + node { + id + dummyBoolean + } + } + } + } + """ + Then the JSON node "data.dummies.edges" should have 1 element + And the JSON node "data.dummies.edges[0].node.dummyBoolean" should be false + + @createSchema + @dropSchema + Scenario: Retrieve a collection filtered using the date filter + Given there are 3 dummy objects with dummyDate + When I send the following GraphQL request: + """ + { + dummies(dummyDate: {after: "2015-04-02"}) { + edges { + node { + id + dummyDate + } + } + } + } + """ + Then the JSON node "data.dummies.edges" should have 1 element + And the JSON node "data.dummies.edges[0].node.dummyDate" should be equal to "2015-04-02T00:00:00+00:00" + + @createSchema + @dropSchema + Scenario: Retrieve a collection filtered using the search filter + Given there are 10 dummy objects + When I send the following GraphQL request: + """ + { + dummies(name: "#2") { + edges { + node { + id + name + } + } + } + } + """ + Then the JSON node "data.dummies.edges" should have 1 element + And the JSON node "data.dummies.edges[0].node.id" should be equal to "/dummies/2" + + @createSchema + @dropSchema + Scenario: Retrieve a collection filtered using the search filter + Given there are 3 dummy objects having each 3 relatedDummies + When I send the following GraphQL request: + """ + { + dummies { + edges { + node { + id + relatedDummies(name: "RelatedDummy13") { + edges { + node { + id + name + } + } + } + } + } + } + } + """ + And the JSON node "data.dummies.edges[0].node.relatedDummies.edges" should have 0 elements + And the JSON node "data.dummies.edges[1].node.relatedDummies.edges" should have 0 elements + And the JSON node "data.dummies.edges[2].node.relatedDummies.edges" should have 1 element + And the JSON node "data.dummies.edges[2].node.relatedDummies.edges[0].node.name" should be equal to "RelatedDummy13" diff --git a/phpstan.neon b/phpstan.neon index 30211297bd9..59549b63e00 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -14,3 +14,5 @@ parameters: - '#Call to an undefined method Doctrine\\Common\\Persistence\\ObjectManager::getConnection\(\)#' - '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Extension\\QueryResult(Item|Collection)ExtensionInterface::supportsResult\(\) invoked with 3 parameters, 1-2 required\.#' - '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Extension\\QueryResult(Item|Collection)ExtensionInterface::getResult\(\) invoked with 4 parameters, 1 required\.#' + - '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\AbstractFilter::filterProperty\(\) invoked with 7 parameters, 5-6 required\.#' + - '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\OrderFilter::filterProperty\(\) invoked with 7 parameters, 5-6 required\.#' diff --git a/src/Bridge/Doctrine/Orm/Extension/FilterExtension.php b/src/Bridge/Doctrine/Orm/Extension/FilterExtension.php index cdd6c3e658f..1429812dd88 100644 --- a/src/Bridge/Doctrine/Orm/Extension/FilterExtension.php +++ b/src/Bridge/Doctrine/Orm/Extension/FilterExtension.php @@ -65,7 +65,7 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator continue; } - $filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); + $filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); } } } diff --git a/src/Bridge/Doctrine/Orm/Filter/AbstractContextAwareFilter.php b/src/Bridge/Doctrine/Orm/Filter/AbstractContextAwareFilter.php new file mode 100644 index 00000000000..a896f59cc6e --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/AbstractContextAwareFilter.php @@ -0,0 +1,36 @@ + + * + * 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\Core\Bridge\Doctrine\Orm\Filter; + +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use Doctrine\ORM\QueryBuilder; + +abstract class AbstractContextAwareFilter extends AbstractFilter implements ContextAwareFilterInterface +{ + /** + * {@inheritdoc} + */ + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = []) + { + if (!isset($context['filters']) || !\is_array($context['filters'])) { + parent::apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); + + return; + } + + foreach ($context['filters'] as $property => $value) { + $this->filterProperty($property, $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); + } + } +} diff --git a/src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php b/src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php index e85fc0bfa08..fb7e7ccd60b 100644 --- a/src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php @@ -41,8 +41,12 @@ abstract class AbstractFilter implements FilterInterface protected $logger; protected $properties; - public function __construct(ManagerRegistry $managerRegistry, RequestStack $requestStack, LoggerInterface $logger = null, array $properties = null) + public function __construct(ManagerRegistry $managerRegistry, RequestStack $requestStack = null, LoggerInterface $logger = null, array $properties = null) { + if (null !== $requestStack) { + @trigger_error(sprintf('Passing an instance of "%s" is deprecated since 2.2. Use "filters" context key instead.', RequestStack::class), E_USER_DEPRECATED); + } + $this->managerRegistry = $managerRegistry; $this->requestStack = $requestStack; $this->logger = $logger ?? new NullLogger(); @@ -52,10 +56,11 @@ public function __construct(ManagerRegistry $managerRegistry, RequestStack $requ /** * {@inheritdoc} */ - public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null) + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null/*, array $context = []*/) { - $request = $this->requestStack->getCurrentRequest(); - if (null === $request) { + @trigger_error(sprintf('Using "%s::apply()" is deprecated since 2.2. Use "%s::apply()" with the "filters" context key instead.', __CLASS__, AbstractContextAwareFilter::class), E_USER_DEPRECATED); + + if (null === $this->requestStack || null === $request = $this->requestStack->getCurrentRequest()) { return; } @@ -66,15 +71,8 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q /** * Passes a property through the filter. - * - * @param string $property - * @param mixed $value - * @param QueryBuilder $queryBuilder - * @param QueryNameGeneratorInterface $queryNameGenerator - * @param string $resourceClass - * @param string|null $operationName */ - abstract protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null); + abstract protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null/*, array $context = []*/); /** * Gets class metadata for the given resource. @@ -248,12 +246,12 @@ protected function splitPropertyParts(string $property/*, string $resourceClass* foreach ($parts as $part) { if ($metadata->hasAssociation($part)) { $metadata = $this->getClassMetadata($metadata->getAssociationTargetClass($part)); - $slice += 1; + ++$slice; } } if ($slice === \count($parts)) { - $slice -= 1; + --$slice; } return [ @@ -264,31 +262,13 @@ protected function splitPropertyParts(string $property/*, string $resourceClass* /** * Extracts properties to filter from the request. - * - * @param Request $request - * - * @return array */ protected function extractProperties(Request $request/*, string $resourceClass*/): array { - if (\func_num_args() > 1) { - $resourceClass = (string) func_get_arg(1); - } else { - if (__CLASS__ !== \get_class($this)) { - $r = new \ReflectionMethod($this, __FUNCTION__); - if (__CLASS__ !== $r->getDeclaringClass()->getName()) { - @trigger_error(sprintf('Method %s() will have a second `$resourceClass` argument in version API Platform 3.0. Not defining it is deprecated since API Platform 2.1.', __FUNCTION__), E_USER_DEPRECATED); - } - } - $resourceClass = null; - } - - if (null !== $properties = $request->attributes->get('_api_filter_common')) { - return $properties; - } + @trigger_error(sprintf('The use of "%s::extractProperties()" is deprecated since 2.2. Use the "filters" key of the context instead.', __CLASS__), E_USER_DEPRECATED); + $resourceClass = \func_num_args() > 1 ? (string) func_get_arg(1) : null; $needsFixing = false; - if (null !== $this->properties) { foreach ($this->properties as $property => $value) { if (($this->isPropertyNested($property, $resourceClass) || $this->isPropertyEmbedded($property, $resourceClass)) && $request->query->has(str_replace('.', '_', $property))) { diff --git a/src/Bridge/Doctrine/Orm/Filter/BooleanFilter.php b/src/Bridge/Doctrine/Orm/Filter/BooleanFilter.php index e811920523f..af080dc22c1 100644 --- a/src/Bridge/Doctrine/Orm/Filter/BooleanFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/BooleanFilter.php @@ -30,8 +30,13 @@ * @author Amrouche Hamza * @author Teoh Han Hui */ -class BooleanFilter extends AbstractFilter +class BooleanFilter extends AbstractContextAwareFilter { + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = []) + { + return parent::apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); // TODO: Change the autogenerated stub + } + /** * {@inheritdoc} */ @@ -72,9 +77,9 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB return; } - if (\in_array($value, ['true', '1'], true)) { + if (\in_array($value, [true, 'true', '1'], true)) { $value = true; - } elseif (\in_array($value, ['false', '0'], true)) { + } elseif (\in_array($value, [false, 'false', '0'], true)) { $value = false; } else { $this->logger->notice('Invalid filter ignored', [ diff --git a/src/Bridge/Doctrine/Orm/Filter/ContextAwareFilterInterface.php b/src/Bridge/Doctrine/Orm/Filter/ContextAwareFilterInterface.php new file mode 100644 index 00000000000..85f4d7ecb95 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/ContextAwareFilterInterface.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); +/* + * This file is part of the API Platform project. + * + * (c) Kévin Dunglas + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Filter; + +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use Doctrine\ORM\QueryBuilder; + +/** + * Context aware filter. + * + * @author Kévin Dunglas + */ +interface ContextAwareFilterInterface extends FilterInterface +{ + /** + * Applies the filter. + */ + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = []); +} diff --git a/src/Bridge/Doctrine/Orm/Filter/DateFilter.php b/src/Bridge/Doctrine/Orm/Filter/DateFilter.php index 159dfd01a15..efd945886e0 100644 --- a/src/Bridge/Doctrine/Orm/Filter/DateFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/DateFilter.php @@ -24,7 +24,7 @@ * @author Kévin Dunglas * @author Théo FIDRY */ -class DateFilter extends AbstractFilter +class DateFilter extends AbstractContextAwareFilter { const PARAMETER_BEFORE = 'before'; const PARAMETER_STRICTLY_BEFORE = 'strictly_before'; diff --git a/src/Bridge/Doctrine/Orm/Filter/ExistsFilter.php b/src/Bridge/Doctrine/Orm/Filter/ExistsFilter.php index d86723f87da..4a8be8b8bee 100644 --- a/src/Bridge/Doctrine/Orm/Filter/ExistsFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/ExistsFilter.php @@ -30,7 +30,7 @@ * * @author Teoh Han Hui */ -class ExistsFilter extends AbstractFilter +class ExistsFilter extends AbstractContextAwareFilter { const QUERY_PARAMETER_KEY = 'exists'; diff --git a/src/Bridge/Doctrine/Orm/Filter/FilterInterface.php b/src/Bridge/Doctrine/Orm/Filter/FilterInterface.php index 582dcf415ab..dfe2da2d041 100644 --- a/src/Bridge/Doctrine/Orm/Filter/FilterInterface.php +++ b/src/Bridge/Doctrine/Orm/Filter/FilterInterface.php @@ -26,11 +26,6 @@ interface FilterInterface extends BaseFilterInterface { /** * Applies the filter. - * - * @param QueryBuilder $queryBuilder - * @param QueryNameGeneratorInterface $queryNameGenerator - * @param string $resourceClass - * @param string|null $operationName */ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null); } diff --git a/src/Bridge/Doctrine/Orm/Filter/NumericFilter.php b/src/Bridge/Doctrine/Orm/Filter/NumericFilter.php index 1fdda0448eb..6e7053ff83c 100644 --- a/src/Bridge/Doctrine/Orm/Filter/NumericFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/NumericFilter.php @@ -29,7 +29,7 @@ * @author Amrouche Hamza * @author Teoh Han Hui */ -class NumericFilter extends AbstractFilter +class NumericFilter extends AbstractContextAwareFilter { /** * Type of numeric in Doctrine. diff --git a/src/Bridge/Doctrine/Orm/Filter/OrderFilter.php b/src/Bridge/Doctrine/Orm/Filter/OrderFilter.php index 3c1c3a8b10c..331310dc2c0 100644 --- a/src/Bridge/Doctrine/Orm/Filter/OrderFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/OrderFilter.php @@ -33,7 +33,7 @@ * @author Kévin Dunglas * @author Théo FIDRY */ -class OrderFilter extends AbstractFilter +class OrderFilter extends AbstractContextAwareFilter { const NULLS_SMALLEST = 'nulls_smallest'; const NULLS_LARGEST = 'nulls_largest'; @@ -53,7 +53,7 @@ class OrderFilter extends AbstractFilter */ protected $orderParameterName; - public function __construct(ManagerRegistry $managerRegistry, RequestStack $requestStack, string $orderParameterName = 'order', LoggerInterface $logger = null, array $properties = null) + public function __construct(ManagerRegistry $managerRegistry, RequestStack $requestStack = null, string $orderParameterName = 'order', LoggerInterface $logger = null, array $properties = null) { if (null !== $properties) { $properties = array_map(function ($propertyOptions) { @@ -73,6 +73,22 @@ public function __construct(ManagerRegistry $managerRegistry, RequestStack $requ $this->orderParameterName = $orderParameterName; } + /** + * {@inheritdoc} + */ + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = []) + { + if (!isset($context['filters'][$this->orderParameterName]) || !\is_array($context['filters'][$this->orderParameterName])) { + parent::apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); + + return; + } + + foreach ($context['filters'][$this->orderParameterName] as $property => $value) { + $this->filterProperty($property, $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); + } + } + /** * {@inheritdoc} */ @@ -143,13 +159,8 @@ protected function filterProperty(string $property, $direction, QueryBuilder $qu */ protected function extractProperties(Request $request/*, string $resourceClass*/): array { - if (null !== $orderAttribute = $request->attributes->get('_api_filter_order')) { - $properties = $orderAttribute; - } elseif (array_key_exists($this->orderParameterName, $commonAttribute = $request->attributes->get('_api_filter_common', []))) { - $properties = $commonAttribute[$this->orderParameterName]; - } else { - $properties = $request->query->get($this->orderParameterName); - } + @trigger_error(sprintf('The use of "%s::extractProperties()" is deprecated since 2.2. Use the "filters" key of the context instead.', __CLASS__), E_USER_DEPRECATED); + $properties = $request->query->get($this->orderParameterName); return \is_array($properties) ? $properties : []; } diff --git a/src/Bridge/Doctrine/Orm/Filter/RangeFilter.php b/src/Bridge/Doctrine/Orm/Filter/RangeFilter.php index 69ca44dbf3c..261738ca6e9 100644 --- a/src/Bridge/Doctrine/Orm/Filter/RangeFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/RangeFilter.php @@ -22,7 +22,7 @@ * * @author Lee Siong Chan */ -class RangeFilter extends AbstractFilter +class RangeFilter extends AbstractContextAwareFilter { const PARAMETER_BETWEEN = 'between'; const PARAMETER_GREATER_THAN = 'gt'; diff --git a/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php b/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php index 301d0d6f96e..87cebdb83c2 100644 --- a/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php @@ -30,7 +30,7 @@ * * @author Kévin Dunglas */ -class SearchFilter extends AbstractFilter +class SearchFilter extends AbstractContextAwareFilter { /** * @var string Exact matching @@ -60,7 +60,7 @@ class SearchFilter extends AbstractFilter protected $iriConverter; protected $propertyAccessor; - public function __construct(ManagerRegistry $managerRegistry, RequestStack $requestStack, IriConverterInterface $iriConverter, PropertyAccessorInterface $propertyAccessor = null, LoggerInterface $logger = null, array $properties = null) + public function __construct(ManagerRegistry $managerRegistry, RequestStack $requestStack = null, IriConverterInterface $iriConverter, PropertyAccessorInterface $propertyAccessor = null, LoggerInterface $logger = null, array $properties = null) { parent::__construct($managerRegistry, $requestStack, $logger, $properties); diff --git a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml index c152a3c37ce..82d59e76390 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -51,7 +51,7 @@ - + null @@ -59,38 +59,38 @@ - + null %api_platform.collection.order_parameter_name% - + null - + null - + null - + null - + null @@ -111,8 +111,6 @@ %api_platform.eager_loading.force_eager% null null - - %api_platform.eager_loading.fetch_partial% diff --git a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml index 3447a31acef..bdc491d1b38 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml @@ -49,6 +49,7 @@ + %api_platform.collection.pagination.enabled% diff --git a/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml b/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml index a095e008349..ccebf5ceb3c 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml @@ -63,6 +63,7 @@ + %api_platform.collection.order_parameter_name% diff --git a/src/EventListener/ReadListener.php b/src/EventListener/ReadListener.php index 954d9bf7a51..e4457d2fba7 100644 --- a/src/EventListener/ReadListener.php +++ b/src/EventListener/ReadListener.php @@ -20,6 +20,7 @@ use ApiPlatform\Core\Exception\RuntimeException; use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; use ApiPlatform\Core\Util\RequestAttributesExtractor; +use ApiPlatform\Core\Util\RequestParser; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -61,7 +62,12 @@ public function onKernelRequest(GetResponseEvent $event) return; } - $context = []; + if (null === $filters = $request->attributes->get('_api_filters')) { + $queryString = $request->getQueryString(); + $filters = $queryString ? RequestParser::parseRequestParams($queryString) : null; + } + + $context = null === $filters ? [] : ['filters' => $filters]; if ($this->serializerContextBuilder) { // Builtin data providers are able to use the serialization context to automatically add join clauses $context['normalization_context'] = $this->serializerContextBuilder->createFromRequest($request, true, $attributes); diff --git a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php b/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php index bcb1442546e..7eac9418f5e 100644 --- a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php @@ -71,15 +71,16 @@ public function __invoke(string $resourceClass = null, string $rootClass = null, } $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - $normalizationContext = $resourceMetadata->getGraphqlAttribute('query', 'normalization_context', [], true); - $normalizationContext['attributes'] = $this->fieldsToAttributes($info); + $dataProviderContext = $resourceMetadata->getGraphqlAttribute('query', 'normalization_context', [], true); + $dataProviderContext['attributes'] = $this->fieldsToAttributes($info); + $dataProviderContext['filters'] = $args; if (isset($source[$rootProperty = $info->fieldName], $source[ItemNormalizer::ITEM_KEY])) { $rootResolvedFields = $this->identifiersExtractor->getIdentifiersFromItem(unserialize($source[ItemNormalizer::ITEM_KEY])); - $subresource = $this->getSubresource($rootClass, $rootResolvedFields, array_keys($rootResolvedFields), $rootProperty, $resourceClass, true, $normalizationContext); + $subresource = $this->getSubresource($rootClass, $rootResolvedFields, array_keys($rootResolvedFields), $rootProperty, $resourceClass, true, $dataProviderContext); $collection = $subresource ?? []; } else { - $collection = $this->collectionDataProvider->getCollection($resourceClass, null, $normalizationContext); + $collection = $this->collectionDataProvider->getCollection($resourceClass, null, $dataProviderContext); } $this->canAccess($this->resourceAccessChecker, $resourceMetadata, $resourceClass, $info, $collection, 'query'); @@ -94,7 +95,7 @@ public function __invoke(string $resourceClass = null, string $rootClass = null, if (!$this->paginationEnabled) { $data = []; foreach ($collection as $index => $object) { - $data[$index] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext); + $data[$index] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $dataProviderContext); } return $data; @@ -117,7 +118,7 @@ public function __invoke(string $resourceClass = null, string $rootClass = null, foreach ($collection as $index => $object) { $data['edges'][$index] = [ - 'node' => $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext), + 'node' => $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $dataProviderContext), 'cursor' => base64_encode((string) ($index + $offset)), ]; } diff --git a/src/GraphQl/Type/SchemaBuilder.php b/src/GraphQl/Type/SchemaBuilder.php index 80c3edac09f..4c52283eb79 100644 --- a/src/GraphQl/Type/SchemaBuilder.php +++ b/src/GraphQl/Type/SchemaBuilder.php @@ -29,6 +29,7 @@ use GraphQL\Type\Definition\Type as GraphQLType; use GraphQL\Type\Definition\WrappingType; use GraphQL\Type\Schema; +use Psr\Container\ContainerInterface; use Symfony\Component\Config\Definition\Exception\InvalidTypeException; use Symfony\Component\PropertyInfo\Type; @@ -53,10 +54,11 @@ final class SchemaBuilder implements SchemaBuilderInterface private $itemResolver; private $itemMutationResolverFactory; private $defaultFieldResolver; + private $filterLocator; private $paginationEnabled; private $graphqlTypes = []; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResolverFactoryInterface $collectionResolverFactory, ResolverFactoryInterface $itemMutationResolverFactory, callable $itemResolver, callable $defaultFieldResolver, bool $paginationEnabled = true) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResolverFactoryInterface $collectionResolverFactory, ResolverFactoryInterface $itemMutationResolverFactory, callable $itemResolver, callable $defaultFieldResolver, ContainerInterface $filterLocator = null, bool $paginationEnabled = true) { $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; $this->propertyMetadataFactory = $propertyMetadataFactory; @@ -66,6 +68,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName $this->itemResolver = $itemResolver; $this->itemMutationResolverFactory = $itemMutationResolverFactory; $this->defaultFieldResolver = $defaultFieldResolver; + $this->filterLocator = $filterLocator; $this->paginationEnabled = $paginationEnabled; } @@ -146,12 +149,12 @@ private function getQueryFields(string $resourceClass, ResourceMetadata $resourc $queryFields = []; $shortName = $resourceMetadata->getShortName(); - if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass)) { + if ($fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass)) { $fieldConfiguration['args'] += ['id' => ['type' => GraphQLType::id()]]; $queryFields[lcfirst($shortName)] = $fieldConfiguration; } - if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass)) { + if ($fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass)) { $queryFields[lcfirst(Inflector::pluralize($shortName))] = $fieldConfiguration; } @@ -166,8 +169,8 @@ private function getMutationFields(string $resourceClass, ResourceMetadata $reso $shortName = $resourceMetadata->getShortName(); $resourceType = new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass); - if ($fieldConfiguration = $this->getResourceFieldConfiguration(ucfirst("{$mutationName}s a $shortName."), $resourceType, $resourceClass, false, $mutationName)) { - $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, $resourceType, $resourceClass, true, $mutationName)]; + if ($fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, ucfirst("{$mutationName}s a $shortName."), $resourceType, $resourceClass, false, $mutationName)) { + $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, $resourceType, $resourceClass, true, $mutationName)]; if (!$resourceType->isCollection()) { $itemMutationResolverFactory = $this->itemMutationResolverFactory; @@ -185,7 +188,7 @@ private function getMutationFields(string $resourceClass, ResourceMetadata $reso * * @return array|null */ - private function getResourceFieldConfiguration(string $fieldDescription = null, Type $type, string $rootResource, bool $input = false, string $mutationName = null) + private function getResourceFieldConfiguration(string $resourceClass, ResourceMetadata $resourceMetadata, string $fieldDescription = null, Type $type, string $rootResource, bool $input = false, string $mutationName = null) { try { if (null === $graphqlType = $this->convertType($type, $input, $mutationName)) { @@ -201,17 +204,43 @@ private function getResourceFieldConfiguration(string $fieldDescription = null, } $args = []; - if ($this->paginationEnabled && !$isInternalGraphqlType && $type->isCollection() && !$input && null === $mutationName) { - $args = [ - 'first' => [ - 'type' => GraphQLType::int(), - 'description' => 'Returns the first n elements from the list.', - ], - 'after' => [ - 'type' => GraphQLType::string(), - 'description' => 'Returns the elements in the list that come after the specified cursor.', - ], - ]; + if (!$input && null === $mutationName && !$isInternalGraphqlType && $type->isCollection()) { + if ($this->paginationEnabled) { + $args = [ + 'first' => [ + 'type' => GraphQLType::int(), + 'description' => 'Returns the first n elements from the list.', + ], + 'after' => [ + 'type' => GraphQLType::string(), + 'description' => 'Returns the elements in the list that come after the specified cursor.', + ], + ]; + } + + foreach ($resourceMetadata->getGraphqlAttribute('query', 'filters', [], true) as $filterId) { + if (!$this->filterLocator->has($filterId)) { + continue; + } + + foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) { + $nullable = isset($value['required']) ? !$value['required'] : true; + $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']); + $graphqlFilterType = $this->convertType($filterType); + + if ('[]' === $newKey = substr($key, -2)) { + $key = $newKey; + $graphqlFilterType = GraphQLType::listOf($graphqlFilterType); + } + + parse_str($key, $parsed); + array_walk_recursive($parsed, function (&$value) use ($graphqlFilterType) { + $value = $graphqlFilterType; + }); + $args = $this->mergeFilterArgs($args, $parsed, $resourceMetadata, $key); + } + } + $args = $this->convertFilterArgsToTypes($args); } if ($isInternalGraphqlType || $input || null !== $mutationName) { @@ -236,6 +265,52 @@ private function getResourceFieldConfiguration(string $fieldDescription = null, return null; } + private function mergeFilterArgs(array $args, array $parsed, ResourceMetadata $resourceMetadata = null, $original = ''): array + { + foreach ($parsed as $key => $value) { + // Never override keys that cannot be merged + if (isset($args[$key]) && !\is_array($args[$key])) { + continue; + } + + if (\is_array($value)) { + $value = $this->mergeFilterArgs($args[$key] ?? [], $value); + if (!isset($value['#name'])) { + $name = (false === $pos = strrpos($original, '[')) ? $original : substr($original, 0, $pos); + $value['#name'] = $resourceMetadata->getShortName().'Filter_'.strtr($name, ['[' => '_', ']' => '', '.' => '__']); + } + } + + $args[$key] = $value; + } + + return $args; + } + + private function convertFilterArgsToTypes(array $args): array + { + foreach ($args as $key => $value) { + if (!\is_array($value) || !isset($value['#name'])) { + continue; + } + + if (isset($this->graphqlTypes[$value['#name']])) { + $args[$key] = $this->graphqlTypes[$value['#name']]; + continue; + } + + $name = $value['#name']; + unset($value['#name']); + + $this->graphqlTypes[$name] = $args[$key] = new InputObjectType([ + 'name' => $name, + 'fields' => $this->convertFilterArgsToTypes($value), + ]); + } + + return $args; + } + /** * Converts a built-in type to its GraphQL equivalent. * @@ -312,8 +387,8 @@ private function getResourceObjectType(string $resourceClass, ResourceMetadata $ 'name' => $shortName, 'description' => $resourceMetadata->getDescription(), 'resolveField' => $this->defaultFieldResolver, - 'fields' => function () use ($resourceClass, $input, $mutationName) { - return $this->getResourceObjectTypeFields($resourceClass, $input, $mutationName); + 'fields' => function () use ($resourceClass, $resourceMetadata, $input, $mutationName) { + return $this->getResourceObjectTypeFields($resourceClass, $resourceMetadata, $input, $mutationName); }, 'interfaces' => [$this->getNodeInterface()], ]; @@ -324,7 +399,7 @@ private function getResourceObjectType(string $resourceClass, ResourceMetadata $ /** * Gets the fields of the type of the given resource. */ - private function getResourceObjectTypeFields(string $resource, bool $input = false, string $mutationName = null): array + private function getResourceObjectTypeFields(string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null): array { $fields = []; $idField = ['type' => GraphQLType::nonNull(GraphQLType::id())]; @@ -341,8 +416,8 @@ private function getResourceObjectTypeFields(string $resource, bool $input = fal $fields['id'] = $idField; } - foreach ($this->propertyNameCollectionFactory->create($resource) as $property) { - $propertyMetadata = $this->propertyMetadataFactory->create($resource, $property); + foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) { + $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property); if ( null === ($propertyType = $propertyMetadata->getType()) || (!$input && null === $mutationName && !$propertyMetadata->isReadable()) @@ -351,7 +426,7 @@ private function getResourceObjectTypeFields(string $resource, bool $input = fal continue; } - if ($fieldConfiguration = $this->getResourceFieldConfiguration($propertyMetadata->getDescription(), $propertyType, $resource, $input, $mutationName)) { + if ($fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, $propertyMetadata->getDescription(), $propertyType, $resourceClass, $input, $mutationName)) { $fields['id' === $property ? '_id' : $property] = $fieldConfiguration; } } diff --git a/src/JsonApi/EventListener/TransformFieldsetsParametersListener.php b/src/JsonApi/EventListener/TransformFieldsetsParametersListener.php index 9b6658fcd2b..64ae80bc791 100644 --- a/src/JsonApi/EventListener/TransformFieldsetsParametersListener.php +++ b/src/JsonApi/EventListener/TransformFieldsetsParametersListener.php @@ -39,7 +39,7 @@ public function onKernelRequest(GetResponseEvent $event) 'jsonapi' !== $request->getRequestFormat() || !($resourceClass = $request->attributes->get('_api_resource_class')) || !($fieldsParameter = $request->query->get('fields')) || - !is_array($fieldsParameter) + !\is_array($fieldsParameter) ) { return; } diff --git a/src/JsonApi/EventListener/TransformFilteringParametersListener.php b/src/JsonApi/EventListener/TransformFilteringParametersListener.php index 97a86150bae..91ca94248b0 100644 --- a/src/JsonApi/EventListener/TransformFilteringParametersListener.php +++ b/src/JsonApi/EventListener/TransformFilteringParametersListener.php @@ -27,15 +27,14 @@ final class TransformFilteringParametersListener public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); - if ( 'jsonapi' !== $request->getRequestFormat() || - null === ($filterParameters = $request->query->get('filter')) || - !is_array($filterParameters) + null === ($filters = $request->query->get('filter')) || + !\is_array($filters) ) { return; } - $request->attributes->set('_api_filter_common', $filterParameters); + $request->attributes->set('_api_filters', $filters); } } diff --git a/src/JsonApi/EventListener/TransformPaginationParametersListener.php b/src/JsonApi/EventListener/TransformPaginationParametersListener.php index 9bb1b4482cc..58edffea67b 100644 --- a/src/JsonApi/EventListener/TransformPaginationParametersListener.php +++ b/src/JsonApi/EventListener/TransformPaginationParametersListener.php @@ -31,7 +31,7 @@ public function onKernelRequest(GetResponseEvent $event) if ( 'jsonapi' !== $request->getRequestFormat() || null === ($pageParameter = $request->query->get('page')) || - !is_array($pageParameter) + !\is_array($pageParameter) ) { return; } diff --git a/src/JsonApi/EventListener/TransformSortingParametersListener.php b/src/JsonApi/EventListener/TransformSortingParametersListener.php index 789080f823a..7bb5f154a7b 100644 --- a/src/JsonApi/EventListener/TransformSortingParametersListener.php +++ b/src/JsonApi/EventListener/TransformSortingParametersListener.php @@ -24,6 +24,13 @@ */ final class TransformSortingParametersListener { + private $orderParameterName; + + public function __construct(string $orderParameterName = 'order') + { + $this->orderParameterName = $orderParameterName; + } + public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); @@ -31,7 +38,7 @@ public function onKernelRequest(GetResponseEvent $event) if ( 'jsonapi' !== $request->getRequestFormat() || null === ($orderParameter = $request->query->get('sort')) || - is_array($orderParameter) + \is_array($orderParameter) ) { return; } @@ -42,7 +49,7 @@ public function onKernelRequest(GetResponseEvent $event) foreach ($orderParametersArray as $orderParameter) { $sorting = 'asc'; - if ('-' === substr($orderParameter, 0, 1)) { + if ('-' === ($orderParameter[0] ?? null)) { $sorting = 'desc'; $orderParameter = substr($orderParameter, 1); } @@ -50,6 +57,8 @@ public function onKernelRequest(GetResponseEvent $event) $transformedOrderParametersArray[$orderParameter] = $sorting; } - $request->attributes->set('_api_filter_order', $transformedOrderParametersArray); + $filters = $request->attributes->get('_api_filters', []); + $filters[$this->orderParameterName] = $transformedOrderParametersArray; + $request->attributes->set('_api_filters', $filters); } } diff --git a/src/Serializer/Filter/FilterInterface.php b/src/Serializer/Filter/FilterInterface.php index 7bd5a56d94f..fba3352eabe 100644 --- a/src/Serializer/Filter/FilterInterface.php +++ b/src/Serializer/Filter/FilterInterface.php @@ -25,11 +25,6 @@ interface FilterInterface extends BaseFilterInterface { /** * Apply a filter to the serializer context. - * - * @param Request $request - * @param bool $normalization - * @param array $attributes - * @param array $context */ public function apply(Request $request, bool $normalization, array $attributes, array &$context); } diff --git a/src/Serializer/Filter/GroupFilter.php b/src/Serializer/Filter/GroupFilter.php index 55ab981bbec..e2df0c5a1ab 100644 --- a/src/Serializer/Filter/GroupFilter.php +++ b/src/Serializer/Filter/GroupFilter.php @@ -39,7 +39,7 @@ public function __construct(string $parameterName = 'groups', bool $overrideDefa */ public function apply(Request $request, bool $normalization, array $attributes, array &$context) { - if (array_key_exists($this->parameterName, $commonAttribute = $request->attributes->get('_api_filter_common', []))) { + if (array_key_exists($this->parameterName, $commonAttribute = $request->attributes->get('_api_filters', []))) { $groups = $commonAttribute[$this->parameterName]; } else { $groups = $request->query->get($this->parameterName); @@ -66,7 +66,7 @@ public function apply(Request $request, bool $normalization, array $attributes, public function getDescription(string $resourceClass): array { return [ - $this->parameterName.'[]' => [ + "$this->parameterName[]" => [ 'property' => null, 'type' => 'string', 'required' => false, diff --git a/src/Serializer/Filter/PropertyFilter.php b/src/Serializer/Filter/PropertyFilter.php index 4aa3e7c4213..6d3e7796c97 100644 --- a/src/Serializer/Filter/PropertyFilter.php +++ b/src/Serializer/Filter/PropertyFilter.php @@ -41,7 +41,7 @@ public function apply(Request $request, bool $normalization, array $attributes, { if (null !== $propertyAttribute = $request->attributes->get('_api_filter_property')) { $properties = $propertyAttribute; - } elseif (array_key_exists($this->parameterName, $commonAttribute = $request->attributes->get('_api_filter_common', []))) { + } elseif (array_key_exists($this->parameterName, $commonAttribute = $request->attributes->get('_api_filters', []))) { $properties = $commonAttribute[$this->parameterName]; } else { $properties = $request->query->get($this->parameterName); @@ -68,7 +68,7 @@ public function apply(Request $request, bool $normalization, array $attributes, public function getDescription(string $resourceClass): array { return [ - $this->parameterName.'[]' => [ + "$this->parameterName[]" => [ 'property' => null, 'type' => 'string', 'required' => false, @@ -113,7 +113,7 @@ private function getProperties(array $properties, array $whitelist = null): arra continue; } - if (isset($whitelist[$key]) && \is_array($value) && $recursiveResult = $this->getProperties($value, $whitelist[$key])) { + if (\is_array($value) && isset($whitelist[$key]) && $recursiveResult = $this->getProperties($value, $whitelist[$key])) { $result[$key] = $recursiveResult; } } diff --git a/src/Util/RequestParser.php b/src/Util/RequestParser.php index 0991c4731a1..323c1baf4a4 100644 --- a/src/Util/RequestParser.php +++ b/src/Util/RequestParser.php @@ -49,7 +49,7 @@ public static function parseAndDuplicateRequest(Request $request): Request * * @author Rok Kralj * - * @see http://stackoverflow.com/a/18209799/1529493 + * @see https://stackoverflow.com/a/18209799/1529493 * * @param string $source * @@ -57,9 +57,9 @@ public static function parseAndDuplicateRequest(Request $request): Request */ public static function parseRequestParams(string $source): array { - // '[' is urlencoded in the input, but we must urldecode it in order + // '[' is urlencoded ('%5B') in the input, but we must urldecode it in order // to find it when replacing names with the regexp below. - $source = str_replace(urlencode('['), '[', $source); + $source = str_replace('%5B', '[', $source); $source = preg_replace_callback( '/(^|(?<=&))[^=[&]+/', diff --git a/tests/Bridge/Doctrine/Orm/Extension/FilterExtensionTest.php b/tests/Bridge/Doctrine/Orm/Extension/FilterExtensionTest.php index c03839cef69..da3ff8f19d7 100644 --- a/tests/Bridge/Doctrine/Orm/Extension/FilterExtensionTest.php +++ b/tests/Bridge/Doctrine/Orm/Extension/FilterExtensionTest.php @@ -41,7 +41,7 @@ public function testApplyToCollectionWithValidFilters() $queryBuilder = $queryBuilderProphecy->reveal(); $ormFilterProphecy = $this->prophesize(FilterInterface::class); - $ormFilterProphecy->apply($queryBuilder, new QueryNameGenerator(), Dummy::class, 'get')->shouldBeCalled(); + $ormFilterProphecy->apply($queryBuilder, new QueryNameGenerator(), Dummy::class, 'get', [])->shouldBeCalled(); $ordinaryFilterProphecy = $this->prophesize(ApiFilterInterface::class); @@ -70,7 +70,7 @@ public function testApplyToCollectionWithValidFiltersAndDeprecatedFilterCollecti $queryBuilder = $queryBuilderProphecy->reveal(); $filterProphecy = $this->prophesize(FilterInterface::class); - $filterProphecy->apply($queryBuilder, new QueryNameGenerator(), Dummy::class, 'get')->shouldBeCalled(); + $filterProphecy->apply($queryBuilder, new QueryNameGenerator(), Dummy::class, 'get', [])->shouldBeCalled(); $orderExtensionTest = new FilterExtension($resourceMetadataFactoryProphecy->reveal(), new FilterCollection(['dummyFilter' => $filterProphecy->reveal()])); $orderExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, 'get'); diff --git a/tests/Bridge/Doctrine/Orm/Filter/AbstractFilterTest.php b/tests/Bridge/Doctrine/Orm/Filter/AbstractFilterTest.php index 90ca57ab7dd..eb1892ea3ca 100644 --- a/tests/Bridge/Doctrine/Orm/Filter/AbstractFilterTest.php +++ b/tests/Bridge/Doctrine/Orm/Filter/AbstractFilterTest.php @@ -13,28 +13,105 @@ namespace ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Filter; -use ApiPlatform\Core\Tests\Fixtures\TestBundle\Doctrine\Orm\Filter\DummyFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\FilterInterface; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use Doctrine\Common\Persistence\ManagerRegistry; -use PHPUnit\Framework\TestCase; +use Doctrine\ORM\EntityRepository; +use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; /** - * Regression test case on issue 1154. - * - * @author Antoine Bluchet + * @author Kévin Dunglas */ -class AbstractFilterTest extends TestCase +abstract class AbstractFilterTest extends KernelTestCase { - public function testSplitPropertiesWithoutResourceClass() + /** + * @var ManagerRegistry + */ + protected $managerRegistry; + + /** + * @var EntityRepository + */ + protected $repository; + + /** + * @var string + */ + protected $resourceClass = Dummy::class; + + /** + * @var string + */ + protected $alias = 'o'; + + /** + * @var string + */ + protected $filterClass; + + protected function setUp() + { + self::bootKernel(); + + $manager = DoctrineTestHelper::createTestEntityManager(); + $this->managerRegistry = self::$kernel->getContainer()->get('doctrine'); + $this->repository = $manager->getRepository(Dummy::class); + } + + /** + * @dataProvider provideApplyTestData + */ + public function testApply(array $properties = null, array $filterParameters, string $expectedDql, array $expectedParameters = null, callable $factory = null) + { + $this->doTestApply(false, $properties, $filterParameters, $expectedDql, $expectedParameters, $factory); + } + + /** + * @group legacy + * @dataProvider provideApplyTestData + */ + public function testApplyRequest(array $properties = null, array $filterParameters, string $expectedDql, array $expectedParameters = null, callable $factory = null) + { + $this->doTestApply(true, $properties, $filterParameters, $expectedDql, $expectedParameters, $factory); + } + + protected function doTestApply(bool $request, array $properties = null, array $filterParameters, string $expectedDql, array $expectedParameters = null, callable $filterFactory = null) { - $managerRegistry = $this->prophesize(ManagerRegistry::class); - $requestStack = $this->prophesize(RequestStack::class); + if (null === $filterFactory) { + $filterFactory = function (ManagerRegistry $managerRegistry, RequestStack $requestStack = null, array $properties = null): FilterInterface { + $filterClass = $this->filterClass; + + return new $filterClass($managerRegistry, $requestStack, null, $properties); + }; + } - $filter = new DummyFilter($managerRegistry->reveal(), $requestStack->reveal()); + $requestStack = null; + if ($request) { + $requestStack = new RequestStack(); + $requestStack->push(Request::create('/api/dummies', 'GET', $filterParameters)); + } - $this->assertEquals($filter->doSplitPropertiesWithoutResourceClass('foo.bar'), [ - 'associations' => ['foo'], - 'field' => 'bar', - ]); + $queryBuilder = $this->repository->createQueryBuilder($this->alias); + $filterCallable = $filterFactory($this->managerRegistry, $requestStack, $properties); + $filterCallable->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass, null, $request ? [] : ['filters' => $filterParameters]); + + $this->assertEquals($expectedDql, $queryBuilder->getQuery()->getDQL()); + + if (null === $expectedParameters) { + return; + } + + foreach ($expectedParameters as $parameterName => $expectedParameterValue) { + $queryParameter = $queryBuilder->getQuery()->getParameter($parameterName); + + $this->assertNotNull($queryParameter, sprintf('Expected query parameter "%s" to be set', $parameterName)); + $this->assertEquals($expectedParameterValue, $queryParameter->getValue(), sprintf('Expected query parameter "%s" to be "%s"', $parameterName, var_export($expectedParameterValue, true))); + } } + + abstract public function provideApplyTestData(): array; } diff --git a/tests/Bridge/Doctrine/Orm/Filter/BooleanFilterTest.php b/tests/Bridge/Doctrine/Orm/Filter/BooleanFilterTest.php index 7e42dcca44c..17c73c682a5 100644 --- a/tests/Bridge/Doctrine/Orm/Filter/BooleanFilterTest.php +++ b/tests/Bridge/Doctrine/Orm/Filter/BooleanFilterTest.php @@ -14,85 +14,23 @@ namespace ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Filter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\BooleanFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; -use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\ORM\EntityRepository; -use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; /** * @author Amrouche Hamza */ -class BooleanFilterTest extends KernelTestCase +class BooleanFilterTest extends AbstractFilterTest { - /** - * @var ManagerRegistry - */ - private $managerRegistry; - - /** - * @var EntityRepository - */ - private $repository; - - /** - * @var string - */ - protected $resourceClass; - - /** - * {@inheritdoc} - */ - protected function setUp() - { - self::bootKernel(); - $manager = DoctrineTestHelper::createTestEntityManager(); - $this->managerRegistry = self::$kernel->getContainer()->get('doctrine'); - $this->repository = $manager->getRepository(Dummy::class); - $this->resourceClass = Dummy::class; - } - - /** - * @dataProvider provideApplyTestData - */ - public function testApply($properties, array $filterParameters, string $expected) - { - $request = Request::create('/api/dummies', 'GET', $filterParameters); - - $requestStack = new RequestStack(); - $requestStack->push($request); - - $queryBuilder = $this->repository->createQueryBuilder('o'); - - $filter = new BooleanFilter( - $this->managerRegistry, - $requestStack, - null, - $properties - ); - - $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass); - $actual = $queryBuilder->getQuery()->getDQL(); - - $this->assertEquals($expected, $actual); - } + protected $filterClass = BooleanFilter::class; public function testGetDescription() { - $filter = new BooleanFilter( - $this->managerRegistry, - new RequestStack(), - null, - [ - 'id' => null, - 'name' => null, - 'foo' => null, - 'dummyBoolean' => null, - ] - ); + $filter = new BooleanFilter($this->managerRegistry, null, null, [ + 'id' => null, + 'name' => null, + 'foo' => null, + 'dummyBoolean' => null, + ]); $this->assertEquals([ 'dummyBoolean' => [ @@ -105,10 +43,7 @@ public function testGetDescription() public function testGetDescriptionDefaultFields() { - $filter = new BooleanFilter( - $this->managerRegistry, - new RequestStack() - ); + $filter = new BooleanFilter($this->managerRegistry); $this->assertEquals([ 'dummyBoolean' => [ @@ -119,16 +54,6 @@ public function testGetDescriptionDefaultFields() ], $filter->getDescription($this->resourceClass)); } - /** - * Provides test data. - * - * Provides 3 parameters: - * - configuration of filterable properties - * - filter parameters - * - expected DQL query - * - * @return array - */ public function provideApplyTestData(): array { return [ diff --git a/tests/Bridge/Doctrine/Orm/Filter/CommonFilterTest.php b/tests/Bridge/Doctrine/Orm/Filter/CommonFilterTest.php new file mode 100644 index 00000000000..03c82e90540 --- /dev/null +++ b/tests/Bridge/Doctrine/Orm/Filter/CommonFilterTest.php @@ -0,0 +1,38 @@ + + * + * 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\Core\Tests\Bridge\Doctrine\Orm\Filter; + +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Doctrine\Orm\Filter\DummyFilter; +use Doctrine\Common\Persistence\ManagerRegistry; +use PHPUnit\Framework\TestCase; + +/** + * Regression test case on issue 1154. + * + * @author Antoine Bluchet + */ +class CommonFilterTest extends TestCase +{ + public function testSplitPropertiesWithoutResourceClass() + { + $managerRegistry = $this->prophesize(ManagerRegistry::class); + + $filter = new DummyFilter($managerRegistry->reveal()); + + $this->assertEquals($filter->doSplitPropertiesWithoutResourceClass('foo.bar'), [ + 'associations' => ['foo'], + 'field' => 'bar', + ]); + } +} diff --git a/tests/Bridge/Doctrine/Orm/Filter/DateFilterTest.php b/tests/Bridge/Doctrine/Orm/Filter/DateFilterTest.php index 953a809d453..903b9aef860 100644 --- a/tests/Bridge/Doctrine/Orm/Filter/DateFilterTest.php +++ b/tests/Bridge/Doctrine/Orm/Filter/DateFilterTest.php @@ -17,10 +17,6 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDate; -use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\ORM\EntityRepository; -use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; @@ -28,89 +24,44 @@ * @author Théo FIDRY * @author Vincent CHALAMON */ -class DateFilterTest extends KernelTestCase +class DateFilterTest extends AbstractFilterTest { - /** - * @var ManagerRegistry - */ - private $managerRegistry; - - /** - * @var EntityRepository - */ - private $repository; + protected $filterClass = DateFilter::class; - /** - * @var string - */ - protected $resourceClass; - - /** - * {@inheritdoc} - */ - protected function setUp() + public function testApplyDate() { - self::bootKernel(); - $manager = DoctrineTestHelper::createTestEntityManager(); - $this->managerRegistry = self::$kernel->getContainer()->get('doctrine'); - $this->repository = $manager->getRepository(Dummy::class); - $this->resourceClass = Dummy::class; + $this->doTestApplyDate(false); } /** - * @dataProvider provideApplyTestData + * @group legacy */ - public function testApply($properties, array $filterParameters, string $expected) + public function testRequestApplyDate() { - $request = Request::create('/api/dummies', 'GET', $filterParameters); - - $requestStack = new RequestStack(); - $requestStack->push($request); - - $queryBuilder = $this->repository->createQueryBuilder('o'); - - $filter = new DateFilter( - $this->managerRegistry, - $requestStack, - null, - $properties - ); - - $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass); - $actual = $queryBuilder->getQuery()->getDQL(); - - $this->assertEquals($expected, $actual); + $this->doTestApplyDate(true); } - public function testApplyDate() + private function doTestApplyDate(bool $request) { - $request = Request::create('/api/dummies', 'GET', [ - 'dummyDate' => [ - 'after' => '2015-04-05', - ], - ]); - $requestStack = new RequestStack(); - $requestStack->push($request); + $filters = ['dummyDate' => ['after' => '2015-04-05']]; + + $requestStack = null; + if ($request) { + $requestStack = new RequestStack(); + $requestStack->push(Request::create('/api/dummies', 'GET', $filters)); + } $queryBuilder = $this->repository->createQueryBuilder('o'); - $filter = new DateFilter( - $this->managerRegistry, - $requestStack, - null, - ['dummyDate' => null] - ); + $filter = new DateFilter($this->managerRegistry, $requestStack, null, ['dummyDate' => null]); + $filter->apply($queryBuilder, new QueryNameGenerator(), DummyDate::class, null, $request ? [] : ['filters' => $filters]); - $filter->apply($queryBuilder, new QueryNameGenerator(), DummyDate::class); $this->assertEquals(new \DateTime('2015-04-05'), $queryBuilder->getParameters()[0]->getValue()); } public function testGetDescription() { - $filter = new DateFilter( - $this->managerRegistry, - new RequestStack() - ); + $filter = new DateFilter($this->managerRegistry); $this->assertEquals([ 'dummyDate[before]' => [ @@ -136,16 +87,6 @@ public function testGetDescription() ], $filter->getDescription($this->resourceClass)); } - /** - * Provides test data. - * - * Provides 3 parameters: - * - configuration of filterable properties - * - filter parameters - * - expected DQL query - * - * @return array - */ public function provideApplyTestData(): array { return [ diff --git a/tests/Bridge/Doctrine/Orm/Filter/ExistsFilterTest.php b/tests/Bridge/Doctrine/Orm/Filter/ExistsFilterTest.php index dd994babb2c..af10960040a 100644 --- a/tests/Bridge/Doctrine/Orm/Filter/ExistsFilterTest.php +++ b/tests/Bridge/Doctrine/Orm/Filter/ExistsFilterTest.php @@ -14,83 +14,18 @@ namespace ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Filter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ExistsFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; -use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\ORM\EntityRepository; -use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; /** * @author Antoine Bluchet */ -class ExistsFilterTest extends KernelTestCase +class ExistsFilterTest extends AbstractFilterTest { - /** - * @var ManagerRegistry - */ - private $managerRegistry; - - /** - * @var EntityRepository - */ - private $repository; - - /** - * @var string - */ - protected $resourceClass; - - /** - * {@inheritdoc} - */ - protected function setUp() - { - self::bootKernel(); - $manager = DoctrineTestHelper::createTestEntityManager(); - $this->managerRegistry = self::$kernel->getContainer()->get('doctrine'); - $this->repository = $manager->getRepository(Dummy::class); - $this->resourceClass = Dummy::class; - } - - /** - * @dataProvider provideApplyTestData - */ - public function testApply($properties, array $filterParameters, string $expected) - { - $request = Request::create('/api/dummies', 'GET', $filterParameters); - - $requestStack = new RequestStack(); - $requestStack->push($request); - - $queryBuilder = $this->repository->createQueryBuilder('o'); - - $filter = new ExistsFilter( - $this->managerRegistry, - $requestStack, - null, - $properties - ); - - $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass); - $actual = $queryBuilder->getQuery()->getDQL(); - - $this->assertEquals($expected, $actual); - } + protected $filterClass = ExistsFilter::class; public function testGetDescription() { - $filter = new ExistsFilter( - $this->managerRegistry, - new RequestStack(), - null, - [ - 'name' => null, - 'description' => null, - ] - ); + $filter = new ExistsFilter($this->managerRegistry, null, null, ['name' => null, 'description' => null]); $this->assertEquals([ 'description[exists]' => [ @@ -103,10 +38,7 @@ public function testGetDescription() public function testGetDescriptionDefaultFields() { - $filter = new ExistsFilter( - $this->managerRegistry, - new RequestStack() - ); + $filter = new ExistsFilter($this->managerRegistry); $this->assertEquals([ 'alias[exists]' => [ @@ -157,16 +89,6 @@ public function testGetDescriptionDefaultFields() ], $filter->getDescription($this->resourceClass)); } - /** - * Provides test data. - * - * Provides 3 parameters: - * - configuration of filterable properties - * - filter parameters - * - expected DQL query - * - * @return array - */ public function provideApplyTestData(): array { return [ diff --git a/tests/Bridge/Doctrine/Orm/Filter/NumericFilterTest.php b/tests/Bridge/Doctrine/Orm/Filter/NumericFilterTest.php index 890e476c409..42e0eedddfd 100644 --- a/tests/Bridge/Doctrine/Orm/Filter/NumericFilterTest.php +++ b/tests/Bridge/Doctrine/Orm/Filter/NumericFilterTest.php @@ -14,85 +14,23 @@ namespace ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Filter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\NumericFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; -use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\ORM\EntityRepository; -use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; /** * @author Amrouche Hamza */ -class NumericFilterTest extends KernelTestCase +class NumericFilterTest extends AbstractFilterTest { - /** - * @var ManagerRegistry - */ - private $managerRegistry; - - /** - * @var EntityRepository - */ - private $repository; - - /** - * @var string - */ - protected $resourceClass; - - /** - * {@inheritdoc} - */ - protected function setUp() - { - self::bootKernel(); - $manager = DoctrineTestHelper::createTestEntityManager(); - $this->managerRegistry = self::$kernel->getContainer()->get('doctrine'); - $this->repository = $manager->getRepository(Dummy::class); - $this->resourceClass = Dummy::class; - } - - /** - * @dataProvider provideApplyTestData - */ - public function testApply($properties, array $filterParameters, string $expected) - { - $request = Request::create('/api/dummies', 'GET', $filterParameters); - - $requestStack = new RequestStack(); - $requestStack->push($request); - - $queryBuilder = $this->repository->createQueryBuilder('o'); - - $filter = new NumericFilter( - $this->managerRegistry, - $requestStack, - null, - $properties - ); - - $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass); - $actual = $queryBuilder->getQuery()->getDQL(); - - $this->assertEquals($expected, $actual); - } + protected $filterClass = NumericFilter::class; public function testGetDescription() { - $filter = new NumericFilter( - $this->managerRegistry, - new RequestStack(), - null, - [ - 'id' => null, - 'name' => null, - 'foo' => null, - 'dummyBoolean' => null, - ] - ); + $filter = new NumericFilter($this->managerRegistry, null, null, [ + 'id' => null, + 'name' => null, + 'foo' => null, + 'dummyBoolean' => null, + ]); $this->assertEquals([ 'id' => [ @@ -105,10 +43,7 @@ public function testGetDescription() public function testGetDescriptionDefaultFields() { - $filter = new NumericFilter( - $this->managerRegistry, - new RequestStack() - ); + $filter = new NumericFilter($this->managerRegistry); $this->assertEquals([ 'id' => [ @@ -129,16 +64,6 @@ public function testGetDescriptionDefaultFields() ], $filter->getDescription($this->resourceClass)); } - /** - * Provides test data. - * - * Provides 3 parameters: - * - configuration of filterable properties - * - filter parameters - * - expected DQL query - * - * @return array - */ public function provideApplyTestData(): array { return [ diff --git a/tests/Bridge/Doctrine/Orm/Filter/OrderFilterTest.php b/tests/Bridge/Doctrine/Orm/Filter/OrderFilterTest.php index 84f3b33d546..aafb3558ea6 100644 --- a/tests/Bridge/Doctrine/Orm/Filter/OrderFilterTest.php +++ b/tests/Bridge/Doctrine/Orm/Filter/OrderFilterTest.php @@ -14,92 +14,21 @@ namespace ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Filter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\ORM\EntityRepository; -use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; /** * @author Théo FIDRY * @author Vincent CHALAMON */ -class OrderFilterTest extends KernelTestCase +class OrderFilterTest extends AbstractFilterTest { - /** - * @var ManagerRegistry - */ - private $managerRegistry; - - /** - * @var EntityRepository - */ - private $repository; - - /** - * @var string - */ - protected $resourceClass; - - /** - * {@inheritdoc} - */ - protected function setUp() - { - self::bootKernel(); - $manager = DoctrineTestHelper::createTestEntityManager(); - $this->managerRegistry = self::$kernel->getContainer()->get('doctrine'); - $this->repository = $manager->getRepository(Dummy::class); - $this->resourceClass = Dummy::class; - } - - /** - * @dataProvider provideApplyTestData - */ - public function testApply(string $orderParameterName, $properties, array $filterParameters, array $attributes, string $expected) - { - $request = Request::create('/api/dummies', 'GET', $filterParameters); - - foreach ($attributes as $key => $value) { - $request->attributes->set($key, $value); - } - - $requestStack = new RequestStack(); - $requestStack->push($request); - - $queryBuilder = $this->repository->createQueryBuilder('o'); - - $filter = new OrderFilter( - $this->managerRegistry, - $requestStack, - $orderParameterName, - null, - $properties - ); - - $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass); - $actual = $queryBuilder->getQuery()->getDQL(); - - $this->assertEquals($expected, $actual); - } + protected $filterClass = OrderFilter::class; public function testGetDescription() { - $filter = new OrderFilter( - $this->managerRegistry, - new RequestStack(), - 'order', - null, - [ - 'id' => null, - 'name' => null, - 'foo' => null, - ] - ); - + $filter = new OrderFilter($this->managerRegistry, null, 'order', null, ['id' => null, 'name' => null, 'foo' => null]); $this->assertEquals([ 'order[id]' => [ 'property' => 'id', @@ -116,11 +45,7 @@ public function testGetDescription() public function testGetDescriptionDefaultFields() { - $filter = new OrderFilter( - $this->managerRegistry, - new RequestStack(), - 'order' - ); + $filter = new OrderFilter($this->managerRegistry); $this->assertEquals([ 'order[id]' => [ @@ -181,79 +106,32 @@ public function testGetDescriptionDefaultFields() ], $filter->getDescription($this->resourceClass)); } - /** - * Provides test data. - * - * Provides 4 parameters: - * - order parameter name - * - configuration of filterable properties - * - filter parameters - * - expected DQL query - * - * @return array - */ public function provideApplyTestData(): array { + $orderFilterFactory = function (ManagerRegistry $managerRegistry, RequestStack $requestStack = null, array $properties = null): OrderFilter { + return new OrderFilter($managerRegistry, $requestStack, 'order', null, $properties); + }; + $customOrderFilterFactory = function (ManagerRegistry $managerRegistry, RequestStack $requestStack = null, array $properties = null): OrderFilter { + return new OrderFilter($managerRegistry, $requestStack, 'customOrder', null, $properties); + }; + return [ 'valid values' => [ - 'order', - [ - 'id' => null, - 'name' => null, - ], - [ - 'order' => [ - 'id' => 'asc', - 'name' => 'desc', - ], - ], - [], - sprintf('SELECT o FROM %s o ORDER BY o.id ASC, o.name DESC', Dummy::class), - ], - 'valid values with order in order filter attribute' => [ - 'order', [ 'id' => null, 'name' => null, ], [ 'order' => [ - 'foo' => 'asc', - 'bar' => 'desc', - ], - ], - [ - '_api_filter_order' => [ 'id' => 'asc', 'name' => 'desc', ], ], sprintf('SELECT o FROM %s o ORDER BY o.id ASC, o.name DESC', Dummy::class), - ], - 'valid values with order in common filter attribute' => [ - 'order', - [ - 'id' => null, - 'name' => null, - ], - [ - 'order' => [ - 'foo' => 'asc', - 'bar' => 'desc', - ], - ], - [ - '_api_filter_common' => [ - 'order' => [ - 'id' => 'asc', - 'name' => 'desc', - ], - ], - ], - sprintf('SELECT o FROM %s o ORDER BY o.id ASC, o.name DESC', Dummy::class), + null, + $orderFilterFactory, ], 'invalid values' => [ - 'order', [ 'id' => null, 'name' => null, @@ -264,11 +142,11 @@ public function provideApplyTestData(): array 'name' => 'invalid', ], ], - [], sprintf('SELECT o FROM %s o ORDER BY o.id ASC', Dummy::class), + null, + $orderFilterFactory, ], 'valid values (properties not enabled)' => [ - 'order', [ 'id' => null, 'name' => null, @@ -279,11 +157,11 @@ public function provideApplyTestData(): array 'alias' => 'asc', ], ], - [], sprintf('SELECT o FROM %s o ORDER BY o.id ASC', Dummy::class), + null, + $orderFilterFactory, ], 'invalid values (properties not enabled)' => [ - 'order', [ 'id' => null, 'name' => null, @@ -295,11 +173,11 @@ public function provideApplyTestData(): array 'alias' => 'invalid', ], ], - [], sprintf('SELECT o FROM %s o ORDER BY o.name ASC', Dummy::class), + null, + $orderFilterFactory, ], 'invalid property (property not enabled)' => [ - 'order', [ 'id' => null, 'name' => null, @@ -309,11 +187,11 @@ public function provideApplyTestData(): array 'unknown' => 'asc', ], ], - [], sprintf('SELECT o FROM %s o', Dummy::class), + null, + $orderFilterFactory, ], 'invalid property (property enabled)' => [ - 'order', [ 'id' => null, 'name' => null, @@ -324,11 +202,11 @@ public function provideApplyTestData(): array 'unknown' => 'asc', ], ], - [], sprintf('SELECT o FROM %s o', Dummy::class), + null, + $orderFilterFactory, ], 'custom order parameter name' => [ - 'customOrder', [ 'id' => null, 'name' => null, @@ -342,11 +220,11 @@ public function provideApplyTestData(): array 'name' => 'desc', ], ], - [], sprintf('SELECT o FROM %s o ORDER BY o.name DESC', Dummy::class), + null, + $customOrderFilterFactory, ], 'valid values (all properties enabled)' => [ - 'order', null, [ 'order' => [ @@ -354,11 +232,11 @@ public function provideApplyTestData(): array 'name' => 'asc', ], ], - [], sprintf('SELECT o FROM %s o ORDER BY o.id ASC, o.name ASC', Dummy::class), + null, + $orderFilterFactory, ], 'nested property' => [ - 'order', [ 'id' => null, 'name' => null, @@ -371,11 +249,11 @@ public function provideApplyTestData(): array 'relatedDummy.symfony' => 'desc', ], ], - [], sprintf('SELECT o FROM %s o INNER JOIN o.relatedDummy relatedDummy_a1 ORDER BY o.id ASC, o.name DESC, relatedDummy_a1.symfony DESC', Dummy::class), + null, + $orderFilterFactory, ], 'empty values with default sort direction' => [ - 'order', [ 'id' => 'asc', 'name' => 'desc', @@ -386,11 +264,11 @@ public function provideApplyTestData(): array 'name' => null, ], ], - [], sprintf('SELECT o FROM %s o ORDER BY o.id ASC, o.name DESC', Dummy::class), + null, + $orderFilterFactory, ], 'nulls_smallest (asc)' => [ - 'order', [ 'dummyDate' => [ 'nulls_comparison' => 'nulls_smallest', @@ -403,11 +281,11 @@ public function provideApplyTestData(): array 'name' => 'desc', ], ], - [], sprintf('SELECT o, CASE WHEN o.dummyDate IS NULL THEN 0 ELSE 1 END AS HIDDEN _o_dummyDate_null_rank FROM %s o ORDER BY _o_dummyDate_null_rank ASC, o.dummyDate ASC, o.name DESC', Dummy::class), + null, + $orderFilterFactory, ], 'nulls_smallest (desc)' => [ - 'order', [ 'dummyDate' => [ 'nulls_comparison' => 'nulls_smallest', @@ -420,11 +298,11 @@ public function provideApplyTestData(): array 'name' => 'desc', ], ], - [], sprintf('SELECT o, CASE WHEN o.dummyDate IS NULL THEN 0 ELSE 1 END AS HIDDEN _o_dummyDate_null_rank FROM %s o ORDER BY _o_dummyDate_null_rank DESC, o.dummyDate DESC, o.name DESC', Dummy::class), + null, + $orderFilterFactory, ], 'nulls_largest (asc)' => [ - 'order', [ 'dummyDate' => [ 'nulls_comparison' => 'nulls_largest', @@ -437,11 +315,11 @@ public function provideApplyTestData(): array 'name' => 'desc', ], ], - [], sprintf('SELECT o, CASE WHEN o.dummyDate IS NULL THEN 0 ELSE 1 END AS HIDDEN _o_dummyDate_null_rank FROM %s o ORDER BY _o_dummyDate_null_rank DESC, o.dummyDate ASC, o.name DESC', Dummy::class), + null, + $orderFilterFactory, ], 'nulls_largest (desc)' => [ - 'order', [ 'dummyDate' => [ 'nulls_comparison' => 'nulls_largest', @@ -454,8 +332,9 @@ public function provideApplyTestData(): array 'name' => 'desc', ], ], - [], sprintf('SELECT o, CASE WHEN o.dummyDate IS NULL THEN 0 ELSE 1 END AS HIDDEN _o_dummyDate_null_rank FROM %s o ORDER BY _o_dummyDate_null_rank ASC, o.dummyDate DESC, o.name DESC', Dummy::class), + null, + $orderFilterFactory, ], ]; } diff --git a/tests/Bridge/Doctrine/Orm/Filter/RangeFilterTest.php b/tests/Bridge/Doctrine/Orm/Filter/RangeFilterTest.php index b9d0de86833..99ecf12de00 100644 --- a/tests/Bridge/Doctrine/Orm/Filter/RangeFilterTest.php +++ b/tests/Bridge/Doctrine/Orm/Filter/RangeFilterTest.php @@ -14,78 +14,18 @@ namespace ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Filter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\RangeFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; -use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\ORM\EntityRepository; -use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; /** * @author Lee Siong Chan */ -class RangeFilterTest extends KernelTestCase +class RangeFilterTest extends AbstractFilterTest { - /** - * @var ManagerRegistry - */ - private $managerRegistry; - - /** - * @var EntityRepository - */ - private $repository; - - /** - * @var string - */ - protected $resourceClass; - - /** - * {@inheritdoc} - */ - protected function setUp() - { - self::bootKernel(); - $manager = DoctrineTestHelper::createTestEntityManager(); - $this->managerRegistry = self::$kernel->getContainer()->get('doctrine'); - $this->repository = $manager->getRepository(Dummy::class); - $this->resourceClass = Dummy::class; - } - - /** - * @dataProvider provideApplyTestData - */ - public function testApply($properties, array $filterParameters, string $expected) - { - $request = Request::create('/api/dummies', 'GET', $filterParameters); - - $requestStack = new RequestStack(); - $requestStack->push($request); - - $queryBuilder = $this->repository->createQueryBuilder('o'); - - $filter = new RangeFilter( - $this->managerRegistry, - $requestStack, - null, - $properties - ); - - $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass); - $actual = $queryBuilder->getQuery()->getDQL(); - - $this->assertEquals($expected, $actual); - } + protected $filterClass = RangeFilter::class; public function testGetDescription() { - $filter = new RangeFilter( - $this->managerRegistry, - new RequestStack() - ); + $filter = new RangeFilter($this->managerRegistry); $this->assertEquals([ 'id[between]' => [ @@ -366,16 +306,6 @@ public function testGetDescription() ], $filter->getDescription($this->resourceClass)); } - /** - * Provides test data. - * - * Provides 3 parameters: - * - configuration of filterable properties - * - filter parameters - * - expected DQL query - * - * @return array - */ public function provideApplyTestData(): array { return [ diff --git a/tests/Bridge/Doctrine/Orm/Filter/SearchFilterTest.php b/tests/Bridge/Doctrine/Orm/Filter/SearchFilterTest.php index 2b0d6ddd614..699be8a3dfe 100644 --- a/tests/Bridge/Doctrine/Orm/Filter/SearchFilterTest.php +++ b/tests/Bridge/Doctrine/Orm/Filter/SearchFilterTest.php @@ -20,58 +20,22 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\ORM\EntityRepository; use Prophecy\Argument; -use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** * @author Julien Deniau * @author Vincent CHALAMON */ -class SearchFilterTest extends KernelTestCase +class SearchFilterTest extends AbstractFilterTest { - const ALIAS = 'oo'; + protected $alias = 'oo'; + protected $filterClass = SearchFilter::class; - /** - * @var ManagerRegistry - */ - private $managerRegistry; - - /** - * @var EntityRepository - */ - private $repository; - - /** - * @var string - */ - protected $resourceClass; - - /** - * @var IriConverterInterface - */ - protected $iriConverter; - - /** - * @var PropertyAccessorInterface - */ - protected $propertyAccessor; - - /** - * {@inheritdoc} - */ - protected function setUp() + protected function filterFactory(ManagerRegistry $managerRegistry, RequestStack $requestStack = null, array $properties = null) { - self::bootKernel(); - $manager = DoctrineTestHelper::createTestEntityManager(); - $this->managerRegistry = self::$kernel->getContainer()->get('doctrine'); - $relatedDummyProphecy = $this->prophesize(RelatedDummy::class); - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getItemFromIri(Argument::type('string'), ['fetch_data' => false])->will(function ($args) use ($relatedDummyProphecy) { @@ -84,68 +48,15 @@ protected function setUp() throw new InvalidArgumentException(); }); - $this->iriConverter = $iriConverterProphecy->reveal(); + $iriConverter = $iriConverterProphecy->reveal(); + $propertyAccessor = self::$kernel->getContainer()->get('test.property_accessor'); - $this->propertyAccessor = self::$kernel->getContainer()->get('test.property_accessor'); - $this->repository = $manager->getRepository(Dummy::class); - $this->resourceClass = Dummy::class; - } - - /** - * @dataProvider provideApplyTestData - */ - public function testApply($properties, array $filterParameters, array $expected) - { - $request = Request::create('/api/dummies', 'GET', $filterParameters); - - $requestStack = new RequestStack(); - $requestStack->push($request); - - $queryBuilder = $this->repository->createQueryBuilder(self::ALIAS); - - $filter = new SearchFilter( - $this->managerRegistry, - $requestStack, - $this->iriConverter, - $this->propertyAccessor, - null, - $properties - ); - - $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass, 'op'); - $actualDql = $queryBuilder->getQuery()->getDQL(); - $expectedDql = $expected['dql']; - - $this->assertEquals($expectedDql, $actualDql); - - if (!empty($expected['parameters'])) { - foreach ($expected['parameters'] as $parameterName => $expectedParameterValue) { - $queryParameter = $queryBuilder->getQuery()->getParameter($parameterName); - - $this->assertNotNull( - $queryParameter, - sprintf('Expected query parameter "%s" to be set', $parameterName) - ); - - $actualParameterValue = $queryParameter->getValue(); - - $this->assertEquals( - $expectedParameterValue, - $actualParameterValue, - sprintf('Expected query parameter "%s" to be "%s"', $parameterName, var_export($expectedParameterValue, true)) - ); - } - } + return new SearchFilter($managerRegistry, $requestStack, $iriConverter, $propertyAccessor, null, $properties); } public function testGetDescription() { - $filter = new SearchFilter( - $this->managerRegistry, - new RequestStack(), - $this->iriConverter, - $this->propertyAccessor - ); + $filter = $this->filterFactory($this->managerRegistry, null); $this->assertEquals([ 'id' => [ @@ -282,25 +193,18 @@ public function testGetDescription() ], ], $filter->getDescription($this->resourceClass)); - $filter = new SearchFilter( - $this->managerRegistry, - new RequestStack(), - $this->iriConverter, - $this->propertyAccessor, - null, - [ - 'id' => null, - 'name' => null, - 'alias' => null, - 'dummy' => null, - 'dummyDate' => null, - 'jsonData' => null, - 'nameConverted' => null, - 'foo' => null, - 'relatedDummies.dummyDate' => null, - 'relatedDummy' => null, - ] - ); + $filter = $this->filterFactory($this->managerRegistry, null, [ + 'id' => null, + 'name' => null, + 'alias' => null, + 'dummy' => null, + 'dummyDate' => null, + 'jsonData' => null, + 'nameConverted' => null, + 'foo' => null, + 'relatedDummies.dummyDate' => null, + 'relatedDummy' => null, + ]); $this->assertEquals([ 'id' => [ @@ -414,18 +318,145 @@ public function testGetDescription() ], $filter->getDescription($this->resourceClass)); } + public function testDoubleJoin() + { + $this->doTestDoubleJoin(false); + } + + /** + * @group legacy + */ + public function testRequestDoubleJoin() + { + $this->doTestDoubleJoin(true); + } + + private function doTestDoubleJoin(bool $request) + { + $filters = ['relatedDummy.symfony' => 'foo']; + + $requestStack = null; + if ($request) { + $requestStack = new RequestStack(); + $requestStack->push(Request::create('/api/dummies', 'GET', $filters)); + } + + $queryBuilder = $this->repository->createQueryBuilder($this->alias); + $filter = $this->filterFactory($this->managerRegistry, $requestStack, ['relatedDummy.symfony' => null]); + + $queryBuilder->innerJoin(sprintf('%s.relatedDummy', $this->alias), 'relateddummy_a1'); + $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass, 'op', $request ? [] : ['filters' => $filters]); + + $actual = strtolower($queryBuilder->getQuery()->getDQL()); + $expected = strtolower(sprintf('SELECT %s FROM %s %1$s inner join %1$s.relatedDummy relateddummy_a1 WHERE relateddummy_a1.symfony = :symfony_p1', $this->alias, Dummy::class)); + $this->assertEquals($actual, $expected); + } + + public function testTripleJoin() + { + $this->doTestTripleJoin(false); + } + /** - * Provides test data. - * - * Provides 3 parameters: - * - configuration of filterable properties - * - filter parameters - * - expected DQL query and parameter values - * - * @return array + * @group legacy */ + public function testRequestTripleJoin() + { + $this->doTestTripleJoin(true); + } + + private function doTestTripleJoin(bool $request) + { + $filters = ['relatedDummy.symfony' => 'foo', 'relatedDummy.thirdLevel.level' => '2']; + + $requestStack = null; + if ($request) { + $requestStack = new RequestStack(); + $requestStack->push(Request::create('/api/dummies', 'GET', $filters)); + } + + $queryBuilder = $this->repository->createQueryBuilder($this->alias); + $filter = $this->filterFactory($this->managerRegistry, $requestStack, ['relatedDummy.symfony' => null, 'relatedDummy.thirdLevel.level' => null]); + + $queryBuilder->innerJoin(sprintf('%s.relatedDummy', $this->alias), 'relateddummy_a1'); + $queryBuilder->innerJoin('relateddummy_a1.thirdLevel', 'thirdLevel_a1'); + + $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass, 'op', $request ? [] : ['filters' => $filters]); + $actual = strtolower($queryBuilder->getQuery()->getDQL()); + $expected = strtolower(sprintf('SELECT %s FROM %s %1$s inner join %1$s.relatedDummy relateddummy_a1 inner join relateddummy_a1.thirdLevel thirdLevel_a1 WHERE relateddummy_a1.symfony = :symfony_p1 and thirdLevel_a1.level = :level_p2', $this->alias, Dummy::class)); + $this->assertEquals($actual, $expected); + } + + public function testJoinLeft() + { + $this->doTestJoinLeft(false); + } + + /** + * @group legacy + */ + public function testRequestJoinLeft() + { + $this->doTestJoinLeft(true); + } + + private function doTestJoinLeft(bool $request) + { + $filters = ['relatedDummy.symfony' => 'foo', 'relatedDummy.thirdLevel.level' => '3']; + + $requestStack = null; + if ($request) { + $requestStack = new RequestStack(); + $requestStack->push(Request::create('/api/dummies', 'GET', $filters)); + } + + $queryBuilder = $this->repository->createQueryBuilder($this->alias); + $queryBuilder->leftJoin(sprintf('%s.relatedDummy', $this->alias), 'relateddummy_a1'); + + $filter = $this->filterFactory($this->managerRegistry, $requestStack, ['relatedDummy.symfony' => null, 'relatedDummy.thirdLevel.level' => null]); + $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass, 'op', $request ? [] : ['filters' => $filters]); + + $actual = strtolower($queryBuilder->getQuery()->getDQL()); + $expected = strtolower(sprintf('SELECT %s FROM %s %1$s left join %1$s.relatedDummy relateddummy_a1 left join relateddummy_a1.thirdLevel thirdLevel_a1 WHERE relateddummy_a1.symfony = :symfony_p1 and thirdLevel_a1.level = :level_p2', $this->alias, Dummy::class)); + $this->assertEquals($actual, $expected); + } + + public function testApplyWithAnotherAlias() + { + $this->doTestApplyWithAnotherAlias(false); + } + + /** + * @group legacy + */ + public function testRequestApplyWithAnotherAlias() + { + $this->doTestApplyWithAnotherAlias(true); + } + + private function doTestApplyWithAnotherAlias(bool $request) + { + $filters = ['name' => 'exact']; + + $requestStack = null; + if ($request) { + $requestStack = new RequestStack(); + $requestStack->push(Request::create('/api/dummies', 'GET', $filters)); + } + + $queryBuilder = $this->repository->createQueryBuilder('somealias'); + + $filter = $this->filterFactory($this->managerRegistry, $requestStack, ['id' => null, 'name' => null]); + $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass, 'op', $request ? [] : ['filters' => $filters]); + + $expectedDql = sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name = :name_p1', 'somealias', Dummy::class); + $this->assertEquals($expectedDql, $queryBuilder->getQuery()->getDQL()); + } + public function provideApplyTestData(): array { + $filterFactory = [$this, 'filterFactory']; + return [ 'exact' => [ [ @@ -435,12 +466,9 @@ public function provideApplyTestData(): array [ 'name' => 'exact', ], - [ - 'dql' => sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name = :name_p1', self::ALIAS, Dummy::class), - 'parameters' => [ - 'name_p1' => 'exact', - ], - ], + sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name = :name_p1', $this->alias, Dummy::class), + ['name_p1' => 'exact'], + $filterFactory, ], 'exact (case insensitive)' => [ [ @@ -450,12 +478,9 @@ public function provideApplyTestData(): array [ 'name' => 'exact', ], - [ - 'dql' => sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) = LOWER(:name_p1)', self::ALIAS, Dummy::class), - 'parameters' => [ - 'name_p1' => 'exact', - ], - ], + sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) = LOWER(:name_p1)', $this->alias, Dummy::class), + ['name_p1' => 'exact'], + $filterFactory, ], 'exact (multiple values)' => [ [ @@ -468,15 +493,14 @@ public function provideApplyTestData(): array 'SENSitive', ], ], + sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name IN (:name_p1)', $this->alias, Dummy::class), [ - 'dql' => sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name IN (:name_p1)', self::ALIAS, Dummy::class), - 'parameters' => [ - 'name_p1' => [ - 'CaSE', - 'SENSitive', - ], + 'name_p1' => [ + 'CaSE', + 'SENSitive', ], ], + $filterFactory, ], 'exact (multiple values; case insensitive)' => [ [ @@ -489,15 +513,14 @@ public function provideApplyTestData(): array 'inSENSitive', ], ], + sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) IN (:name_p1)', $this->alias, Dummy::class), [ - 'dql' => sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) IN (:name_p1)', self::ALIAS, Dummy::class), - 'parameters' => [ - 'name_p1' => [ - 'case', - 'insensitive', - ], + 'name_p1' => [ + 'case', + 'insensitive', ], ], + $filterFactory, ], 'invalid property' => [ [ @@ -507,10 +530,9 @@ public function provideApplyTestData(): array [ 'foo' => 'exact', ], - [ - 'dql' => sprintf('SELECT %s FROM %s %1$s', self::ALIAS, Dummy::class), - 'parameters' => [], - ], + sprintf('SELECT %s FROM %s %1$s', $this->alias, Dummy::class), + [], + $filterFactory, ], 'invalid values for relations' => [ [ @@ -524,12 +546,9 @@ public function provideApplyTestData(): array 'relatedDummy' => ['foo'], 'relatedDummies' => [['foo']], ], - [ - 'dql' => sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name = :name_p1 AND %1$s.relatedDummy = :relatedDummy_p2', self::ALIAS, Dummy::class), - 'parameters' => [ - 'relatedDummy_p2' => 'foo', - ], - ], + sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name = :name_p1 AND %1$s.relatedDummy = :relatedDummy_p2', $this->alias, Dummy::class), + ['relatedDummy_p2' => 'foo'], + $filterFactory, ], 'partial' => [ [ @@ -539,12 +558,9 @@ public function provideApplyTestData(): array [ 'name' => 'partial', ], - [ - 'dql' => sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name LIKE CONCAT(\'%%\', :name_p1, \'%%\')', self::ALIAS, Dummy::class), - 'parameters' => [ - 'name_p1' => 'partial', - ], - ], + sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name LIKE CONCAT(\'%%\', :name_p1, \'%%\')', $this->alias, Dummy::class), + ['name_p1' => 'partial'], + $filterFactory, ], 'partial (case insensitive)' => [ [ @@ -554,12 +570,9 @@ public function provideApplyTestData(): array [ 'name' => 'partial', ], - [ - 'dql' => sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) LIKE LOWER(CONCAT(\'%%\', :name_p1, \'%%\'))', self::ALIAS, Dummy::class), - 'parameters' => [ - 'name_p1' => 'partial', - ], - ], + sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) LIKE LOWER(CONCAT(\'%%\', :name_p1, \'%%\'))', $this->alias, Dummy::class), + ['name_p1' => 'partial'], + $filterFactory, ], 'start' => [ [ @@ -569,12 +582,9 @@ public function provideApplyTestData(): array [ 'name' => 'partial', ], - [ - 'dql' => sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name LIKE CONCAT(:name_p1, \'%%\')', self::ALIAS, Dummy::class), - 'parameters' => [ - 'name_p1' => 'partial', - ], - ], + sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name LIKE CONCAT(:name_p1, \'%%\')', $this->alias, Dummy::class), + ['name_p1' => 'partial'], + $filterFactory, ], 'start (case insensitive)' => [ [ @@ -584,12 +594,9 @@ public function provideApplyTestData(): array [ 'name' => 'partial', ], - [ - 'dql' => sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) LIKE LOWER(CONCAT(:name_p1, \'%%\'))', self::ALIAS, Dummy::class), - 'parameters' => [ - 'name_p1' => 'partial', - ], - ], + sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) LIKE LOWER(CONCAT(:name_p1, \'%%\'))', $this->alias, Dummy::class), + ['name_p1' => 'partial'], + $filterFactory, ], 'end' => [ [ @@ -599,12 +606,9 @@ public function provideApplyTestData(): array [ 'name' => 'partial', ], - [ - 'dql' => sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name LIKE CONCAT(\'%%\', :name_p1)', self::ALIAS, Dummy::class), - 'parameters' => [ - 'name_p1' => 'partial', - ], - ], + sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name LIKE CONCAT(\'%%\', :name_p1)', $this->alias, Dummy::class), + ['name_p1' => 'partial'], + $filterFactory, ], 'end (case insensitive)' => [ [ @@ -614,12 +618,9 @@ public function provideApplyTestData(): array [ 'name' => 'partial', ], - [ - 'dql' => sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) LIKE LOWER(CONCAT(\'%%\', :name_p1))', self::ALIAS, Dummy::class), - 'parameters' => [ - 'name_p1' => 'partial', - ], - ], + sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) LIKE LOWER(CONCAT(\'%%\', :name_p1))', $this->alias, Dummy::class), + ['name_p1' => 'partial'], + $filterFactory, ], 'word_start' => [ [ @@ -629,12 +630,9 @@ public function provideApplyTestData(): array [ 'name' => 'partial', ], - [ - 'dql' => sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name LIKE CONCAT(:name_p1, \'%%\') OR %1$s.name LIKE CONCAT(\'%% \', :name_p1, \'%%\')', self::ALIAS, Dummy::class), - 'parameters' => [ - 'name_p1' => 'partial', - ], - ], + sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name LIKE CONCAT(:name_p1, \'%%\') OR %1$s.name LIKE CONCAT(\'%% \', :name_p1, \'%%\')', $this->alias, Dummy::class), + ['name_p1' => 'partial'], + $filterFactory, ], 'word_start (case insensitive)' => [ [ @@ -644,12 +642,9 @@ public function provideApplyTestData(): array [ 'name' => 'partial', ], - [ - 'dql' => sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) LIKE LOWER(CONCAT(:name_p1, \'%%\')) OR LOWER(%1$s.name) LIKE LOWER(CONCAT(\'%% \', :name_p1, \'%%\'))', self::ALIAS, Dummy::class), - 'parameters' => [ - 'name_p1' => 'partial', - ], - ], + sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) LIKE LOWER(CONCAT(:name_p1, \'%%\')) OR LOWER(%1$s.name) LIKE LOWER(CONCAT(\'%% \', :name_p1, \'%%\'))', $this->alias, Dummy::class), + ['name_p1' => 'partial'], + $filterFactory, ], 'invalid value for relation' => [ [ @@ -660,12 +655,9 @@ public function provideApplyTestData(): array [ 'relatedDummy' => 'exact', ], - [ - 'dql' => sprintf('SELECT %s FROM %s %1$s WHERE %1$s.relatedDummy = :relatedDummy_p1', self::ALIAS, Dummy::class), - 'parameters' => [ - 'relatedDummy_p1' => 'exact', - ], - ], + sprintf('SELECT %s FROM %s %1$s WHERE %1$s.relatedDummy = :relatedDummy_p1', $this->alias, Dummy::class), + ['relatedDummy_p1' => 'exact'], + $filterFactory, ], 'IRI value for relation' => [ [ @@ -676,12 +668,9 @@ public function provideApplyTestData(): array [ 'relatedDummy.id' => '/related_dummies/1', ], - [ - 'dql' => sprintf('SELECT %s FROM %s %1$s INNER JOIN %1$s.relatedDummy relatedDummy_a1 WHERE relatedDummy_a1.id = :id_p1', self::ALIAS, Dummy::class), - 'parameters' => [ - 'id_p1' => 1, - ], - ], + sprintf('SELECT %s FROM %s %1$s INNER JOIN %1$s.relatedDummy relatedDummy_a1 WHERE relatedDummy_a1.id = :id_p1', $this->alias, Dummy::class), + ['id_p1' => 1], + $filterFactory, ], 'mixed IRI and entity ID values for relations' => [ [ @@ -694,13 +683,12 @@ public function provideApplyTestData(): array 'relatedDummy' => ['/related_dummies/1', '2'], 'relatedDummies' => '1', ], + sprintf('SELECT %s FROM %s %1$s INNER JOIN %1$s.relatedDummies relatedDummies_a1 WHERE %1$s.relatedDummy IN (:relatedDummy_p1) AND relatedDummies_a1.id = :relatedDummies_p2', $this->alias, Dummy::class), [ - 'dql' => sprintf('SELECT %s FROM %s %1$s INNER JOIN %1$s.relatedDummies relatedDummies_a1 WHERE %1$s.relatedDummy IN (:relatedDummy_p1) AND relatedDummies_a1.id = :relatedDummies_p2', self::ALIAS, Dummy::class), - 'parameters' => [ - 'relatedDummy_p1' => [1, 2], - 'relatedDummies_p2' => 1, - ], + 'relatedDummy_p1' => [1, 2], + 'relatedDummies_p2' => 1, ], + $filterFactory, ], 'nested property' => [ [ @@ -712,113 +700,13 @@ public function provideApplyTestData(): array 'name' => 'exact', 'relatedDummy.symfony' => 'exact', ], + sprintf('SELECT %s FROM %s %1$s INNER JOIN %1$s.relatedDummy relatedDummy_a1 WHERE %1$s.name = :name_p1 AND relatedDummy_a1.symfony = :symfony_p2', $this->alias, Dummy::class), [ - 'dql' => sprintf('SELECT %s FROM %s %1$s INNER JOIN %1$s.relatedDummy relatedDummy_a1 WHERE %1$s.name = :name_p1 AND relatedDummy_a1.symfony = :symfony_p2', self::ALIAS, Dummy::class), - 'parameters' => [ - 'name_p1' => 'exact', - 'symfony_p2' => 'exact', - ], + 'name_p1' => 'exact', + 'symfony_p2' => 'exact', ], + $filterFactory, ], ]; } - - public function testDoubleJoin() - { - $request = Request::create('/api/dummies', 'GET', ['relatedDummy.symfony' => 'foo']); - $requestStack = new RequestStack(); - $requestStack->push($request); - $queryBuilder = $this->repository->createQueryBuilder(self::ALIAS); - $filter = new SearchFilter( - $this->managerRegistry, - $requestStack, - $this->iriConverter, - $this->propertyAccessor, - null, - ['relatedDummy.symfony' => null] - ); - - $queryBuilder->innerJoin(sprintf('%s.relatedDummy', self::ALIAS), 'relateddummy_a1'); - - $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass, 'op'); - $actual = strtolower($queryBuilder->getQuery()->getDQL()); - $expected = strtolower(sprintf('SELECT %s FROM %s %1$s inner join %1$s.relatedDummy relateddummy_a1 WHERE relateddummy_a1.symfony = :symfony_p1', self::ALIAS, Dummy::class)); - $this->assertEquals($actual, $expected); - } - - public function testTripleJoin() - { - $request = Request::create('/api/dummies', 'GET', ['relatedDummy.symfony' => 'foo', 'relatedDummy.thirdLevel.level' => '2']); - $requestStack = new RequestStack(); - $requestStack->push($request); - $queryBuilder = $this->repository->createQueryBuilder(self::ALIAS); - $filter = new SearchFilter( - $this->managerRegistry, - $requestStack, - $this->iriConverter, - $this->propertyAccessor, - null, - ['relatedDummy.symfony' => null, 'relatedDummy.thirdLevel.level' => null] - ); - - $queryBuilder->innerJoin(sprintf('%s.relatedDummy', self::ALIAS), 'relateddummy_a1'); - $queryBuilder->innerJoin('relateddummy_a1.thirdLevel', 'thirdLevel_a1'); - - $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass, 'op'); - $actual = strtolower($queryBuilder->getQuery()->getDQL()); - $expected = strtolower(sprintf('SELECT %s FROM %s %1$s inner join %1$s.relatedDummy relateddummy_a1 inner join relateddummy_a1.thirdLevel thirdLevel_a1 WHERE relateddummy_a1.symfony = :symfony_p1 and thirdLevel_a1.level = :level_p2', self::ALIAS, Dummy::class)); - $this->assertEquals($actual, $expected); - } - - public function testJoinLeft() - { - $request = Request::create('/api/dummies', 'GET', ['relatedDummy.symfony' => 'foo', 'relatedDummy.thirdLevel.level' => '3']); - $requestStack = new RequestStack(); - $requestStack->push($request); - $queryBuilder = $this->repository->createQueryBuilder(self::ALIAS); - $queryBuilder->leftJoin(sprintf('%s.relatedDummy', self::ALIAS), 'relateddummy_a1'); - - $filter = new SearchFilter( - $this->managerRegistry, - $requestStack, - $this->iriConverter, - $this->propertyAccessor, - null, - ['relatedDummy.symfony' => null, 'relatedDummy.thirdLevel.level' => null] - ); - - $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass, 'op'); - $actual = strtolower($queryBuilder->getQuery()->getDQL()); - $expected = strtolower(sprintf('SELECT %s FROM %s %1$s left join %1$s.relatedDummy relateddummy_a1 left join relateddummy_a1.thirdLevel thirdLevel_a1 WHERE relateddummy_a1.symfony = :symfony_p1 and thirdLevel_a1.level = :level_p2', self::ALIAS, Dummy::class)); - $this->assertEquals($actual, $expected); - } - - public function testApplyWithAnotherAlias() - { - $request = Request::create('/api/dummies', 'GET', ['name' => 'exact']); - - $requestStack = new RequestStack(); - $requestStack->push($request); - - $queryBuilder = $this->repository->createQueryBuilder('somealias'); - - $filter = new SearchFilter( - $this->managerRegistry, - $requestStack, - $this->iriConverter, - $this->propertyAccessor, - null, - [ - 'id' => null, - 'name' => null, - ] - ); - - $filter->apply($queryBuilder, new QueryNameGenerator(), $this->resourceClass, 'op'); - $actualDql = $queryBuilder->getQuery()->getDQL(); - - $expectedDql = sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name = :name_p1', 'somealias', Dummy::class); - - $this->assertEquals($expectedDql, $actualDql); - } } diff --git a/tests/Bridge/Symfony/Bundle/Command/SwaggerCommandTest.php b/tests/Bridge/Symfony/Bundle/Command/SwaggerCommandTest.php index f0383f24ac4..c8a381d91e5 100644 --- a/tests/Bridge/Symfony/Bundle/Command/SwaggerCommandTest.php +++ b/tests/Bridge/Symfony/Bundle/Command/SwaggerCommandTest.php @@ -22,6 +22,11 @@ */ class SwaggerCommandTest extends KernelTestCase { + /** + * The legacy group a workaround to prevent the deprecation triggered by the @Filter annotation (because it autowires filters). + * + * @group legacy + */ public function testExecute() { self::bootKernel(); diff --git a/tests/EventListener/ReadListenerTest.php b/tests/EventListener/ReadListenerTest.php index 9f0d237426a..74e569383ff 100644 --- a/tests/EventListener/ReadListenerTest.php +++ b/tests/EventListener/ReadListenerTest.php @@ -92,7 +92,7 @@ public function testRetrieveCollectionPost() public function testRetrieveCollectionGet() { $collectionDataProvider = $this->prophesize(CollectionDataProviderInterface::class); - $collectionDataProvider->getCollection('Foo', 'get', [])->willReturn([])->shouldBeCalled(); + $collectionDataProvider->getCollection('Foo', 'get', ['filters' => ['foo' => 'bar']])->willReturn([])->shouldBeCalled(); $itemDataProvider = $this->prophesize(ItemDataProviderInterface::class); $itemDataProvider->getItem()->shouldNotBeCalled(); @@ -100,7 +100,7 @@ public function testRetrieveCollectionGet() $subresourceDataProvider = $this->prophesize(SubresourceDataProviderInterface::class); $subresourceDataProvider->getSubresource()->shouldNotBeCalled(); - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json']); + $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json'], [], [], ['QUERY_STRING' => 'foo=bar']); $request->setMethod(Request::METHOD_GET); $event = $this->prophesize(GetResponseEvent::class); diff --git a/tests/GraphQl/Resolver/Factory/CollectionResolverFactoryTest.php b/tests/GraphQl/Resolver/Factory/CollectionResolverFactoryTest.php index 1c32baa7149..d8000100734 100644 --- a/tests/GraphQl/Resolver/Factory/CollectionResolverFactoryTest.php +++ b/tests/GraphQl/Resolver/Factory/CollectionResolverFactoryTest.php @@ -118,7 +118,7 @@ public function testCreateCollectionResolver(string $cursor, array $expectedCurs $factory = $this->createCollectionResolverFactory([ 'Object1', 'Object2', - ], [], [], true); + ], [], [], true, $cursor); $resolver = $factory(RelatedDummy::class, Dummy::class, 'operationName'); @@ -162,7 +162,8 @@ public function testCreatePaginatorCollectionResolver() $collectionPaginatorProphecy->count()->willReturn(8); $collectionPaginatorProphecy->getItemsPerPage()->willReturn(8); - $resolverFactory = $this->createCollectionResolverFactory($collectionPaginatorProphecy->reveal(), [], [], true); + $cursor = 'MQ=='; + $resolverFactory = $this->createCollectionResolverFactory($collectionPaginatorProphecy->reveal(), [], [], true, $cursor); $resolver = $resolverFactory(RelatedDummy::class, Dummy::class, 'operationName'); $resolveInfo = new ResolveInfo([]); @@ -171,18 +172,20 @@ public function testCreatePaginatorCollectionResolver() $this->assertEquals( ['edges' => [['node' => 'normalizedObject1', 'cursor' => 'Mg==']], 'pageInfo' => ['endCursor' => 'MTY=', 'hasNextPage' => true]], - $resolver(null, ['after' => 'MQ=='], null, $resolveInfo) + $resolver(null, ['after' => $cursor], null, $resolveInfo) ); } /** * @param array|\Iterator $collection */ - private function createCollectionResolverFactory($collection, array $subcollection, array $identifiers, bool $paginationEnabled): CollectionResolverFactory + private function createCollectionResolverFactory($collection, array $subcollection, array $identifiers, bool $paginationEnabled, string $cursor = null): CollectionResolverFactory { $collectionDataProviderProphecy = $this->prophesize(CollectionDataProviderInterface::class); - $collectionDataProviderProphecy->getCollection(Dummy::class, null, ['groups' => ['foo'], 'attributes' => []])->willReturn($collection); - $collectionDataProviderProphecy->getCollection(RelatedDummy::class, null, ['groups' => ['foo'], 'attributes' => []])->willReturn($collection); + + $filters = $cursor ? ['after' => $cursor] : []; + $collectionDataProviderProphecy->getCollection(Dummy::class, null, ['groups' => ['foo'], 'attributes' => [], 'filters' => []])->willReturn($collection); + $collectionDataProviderProphecy->getCollection(RelatedDummy::class, null, ['groups' => ['foo'], 'attributes' => [], 'filters' => $filters])->willReturn($collection); $subresourceDataProviderProphecy = $this->prophesize(SubresourceDataProviderInterface::class); $subresourceDataProviderProphecy->getSubresource(RelatedDummy::class, $identifiers, [ @@ -191,6 +194,7 @@ private function createCollectionResolverFactory($collection, array $subcollecti 'collection' => true, 'groups' => ['foo'], 'attributes' => [], + 'filters' => [], ])->willReturn($subcollection); $normalizerProphecy = $this->prophesize(NormalizerInterface::class); diff --git a/tests/GraphQl/Type/SchemaBuilderTest.php b/tests/GraphQl/Type/SchemaBuilderTest.php index 01350d9ad63..1ef945a4fb8 100644 --- a/tests/GraphQl/Type/SchemaBuilderTest.php +++ b/tests/GraphQl/Type/SchemaBuilderTest.php @@ -231,6 +231,7 @@ private function createSchemaBuilder($propertyMetadataMockBuilder, bool $paginat $itemMutationResolverFactoryProphecy->reveal(), function () {}, function () {}, + null, $paginationEnabled ); } diff --git a/tests/JsonApi/EventListener/TransformFilteringParametersListenerTest.php b/tests/JsonApi/EventListener/TransformFilteringParametersListenerTest.php index 760ad51e516..72a76c69a59 100644 --- a/tests/JsonApi/EventListener/TransformFilteringParametersListenerTest.php +++ b/tests/JsonApi/EventListener/TransformFilteringParametersListenerTest.php @@ -77,7 +77,7 @@ public function testOnKernelRequest() $this->listener->onKernelRequest($eventProphecy->reveal()); - $expectedRequest = new Request(['filter' => ['foo' => 'bar', 'baz' => 'qux']], [], ['_api_filter_common' => ['foo' => 'bar', 'baz' => 'qux']]); + $expectedRequest = new Request(['filter' => ['foo' => 'bar', 'baz' => 'qux']], [], ['_api_filters' => ['foo' => 'bar', 'baz' => 'qux']]); $expectedRequest->setRequestFormat('jsonapi'); $this->assertEquals($expectedRequest, $request); diff --git a/tests/JsonApi/EventListener/TransformSortingParametersListenerTest.php b/tests/JsonApi/EventListener/TransformSortingParametersListenerTest.php index a8456d3fd00..d1d9f51bf34 100644 --- a/tests/JsonApi/EventListener/TransformSortingParametersListenerTest.php +++ b/tests/JsonApi/EventListener/TransformSortingParametersListenerTest.php @@ -77,7 +77,7 @@ public function testOnKernelRequest() $this->listener->onKernelRequest($eventProphecy->reveal()); - $expectedRequest = new Request(['sort' => 'foo,-bar,-baz,qux'], [], ['_api_filter_order' => ['foo' => 'asc', 'bar' => 'desc', 'baz' => 'desc', 'qux' => 'asc']]); + $expectedRequest = new Request(['sort' => 'foo,-bar,-baz,qux'], [], ['_api_filters' => ['order' => ['foo' => 'asc', 'bar' => 'desc', 'baz' => 'desc', 'qux' => 'asc']]]); $expectedRequest->setRequestFormat('jsonapi'); $this->assertEquals($expectedRequest, $request); diff --git a/tests/Serializer/Filter/GroupFilterTest.php b/tests/Serializer/Filter/GroupFilterTest.php index b849e3af395..26cae3c6b91 100644 --- a/tests/Serializer/Filter/GroupFilterTest.php +++ b/tests/Serializer/Filter/GroupFilterTest.php @@ -78,9 +78,9 @@ public function testApplyWithGroupsWhitelistWithOverriding() $this->assertEquals([AbstractNormalizer::GROUPS => ['foo', 'baz']], $context); } - public function testApplyWithGroupsInCommonFilterAttribute() + public function testApplyWithGroupsInFilterAttribute() { - $request = new Request(['groups' => ['foo', 'bar', 'baz']], [], ['_api_filter_common' => ['groups' => ['fooz']]]); + $request = new Request(['groups' => ['foo', 'bar', 'baz']], [], ['_api_filters' => ['groups' => ['fooz']]]); $context = ['groups' => ['foo', 'qux']]; $groupFilter = new GroupFilter(); diff --git a/tests/Serializer/Filter/PropertyFilterTest.php b/tests/Serializer/Filter/PropertyFilterTest.php index 0e87255586f..3a458dd6f60 100644 --- a/tests/Serializer/Filter/PropertyFilterTest.php +++ b/tests/Serializer/Filter/PropertyFilterTest.php @@ -101,18 +101,7 @@ public function testApplyWithoutPropertiesWhitelistWithOverriding() public function testApplyWithPropertiesInPropertyFilterAttribute() { - $request = new Request(['properties' => ['foo', 'bar', 'baz']], [], ['_api_filter_property' => ['fooz']]); - $context = ['attributes' => ['foo', 'qux']]; - - $propertyFilter = new PropertyFilter(); - $propertyFilter->apply($request, true, [], $context); - - $this->assertEquals(['attributes' => ['foo', 'qux', 'fooz']], $context); - } - - public function testApplyWithPropertiesInCommonFilterAttribute() - { - $request = new Request(['properties' => ['foo', 'bar', 'baz']], [], ['_api_filter_common' => ['properties' => ['fooz']]]); + $request = new Request(['properties' => ['foo', 'bar', 'baz']], [], ['_api_filters' => ['properties' => ['fooz']]]); $context = ['attributes' => ['foo', 'qux']]; $propertyFilter = new PropertyFilter();