Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions features/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down
94 changes: 94 additions & 0 deletions features/graphql/filters.feature
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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\.#'
2 changes: 1 addition & 1 deletion src/Bridge/Doctrine/Orm/Extension/FilterExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
continue;
}

$filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName);
$filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
}
}
}
36 changes: 36 additions & 0 deletions src/Bridge/Doctrine/Orm/Filter/AbstractContextAwareFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\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);
}
}
}
48 changes: 14 additions & 34 deletions src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
}

Expand All @@ -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 = []*/);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dunglas The commented out $context in apply and filterProperty is a bit confusing. I see that AbstractContextAwareFilter extends from this class and overrides apply introducing array $context = [], and then calls $this->filterProperty(..., $context);.
How about filterProperty, should it also be overridden in AbstractContextAwareFilter with adding array $context = []?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh good catch I think it should indeed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As @meyerbaptiste reminds us, we need PHP 7.2 for this: https://3v4l.org/ktgkR


/**
* Gets class metadata for the given resource.
Expand Down Expand Up @@ -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 [
Expand All @@ -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))) {
Expand Down
11 changes: 8 additions & 3 deletions src/Bridge/Doctrine/Orm/Filter/BooleanFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,13 @@
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
* @author Teoh Han Hui <teohhanhui@gmail.com>
*/
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}
*/
Expand Down Expand Up @@ -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', [
Expand Down
38 changes: 38 additions & 0 deletions src/Bridge/Doctrine/Orm/Filter/ContextAwareFilterInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);
/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

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 <dunglas@gmail.com>
*/
interface ContextAwareFilterInterface extends FilterInterface
{
/**
* Applies the filter.
*/
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = []);
}
2 changes: 1 addition & 1 deletion src/Bridge/Doctrine/Orm/Filter/DateFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Théo FIDRY <theo.fidry@gmail.com>
*/
class DateFilter extends AbstractFilter
class DateFilter extends AbstractContextAwareFilter
{
const PARAMETER_BEFORE = 'before';
const PARAMETER_STRICTLY_BEFORE = 'strictly_before';
Expand Down
2 changes: 1 addition & 1 deletion src/Bridge/Doctrine/Orm/Filter/ExistsFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
*
* @author Teoh Han Hui <teohhanhui@gmail.com>
*/
class ExistsFilter extends AbstractFilter
class ExistsFilter extends AbstractContextAwareFilter
{
const QUERY_PARAMETER_KEY = 'exists';

Expand Down
5 changes: 0 additions & 5 deletions src/Bridge/Doctrine/Orm/Filter/FilterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
2 changes: 1 addition & 1 deletion src/Bridge/Doctrine/Orm/Filter/NumericFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
* @author Teoh Han Hui <teohhanhui@gmail.com>
*/
class NumericFilter extends AbstractFilter
class NumericFilter extends AbstractContextAwareFilter
{
/**
* Type of numeric in Doctrine.
Expand Down
Loading