Skip to content

Commit d3ab73b

Browse files
author
Lee Siong Chan
committed
Add RangeFilter
Filter collections from range, can be either (between / lt / lte / gt / gte). This is useful when dealing with min price and max price, duration and etc.
1 parent 61faae2 commit d3ab73b

File tree

15 files changed

+979
-4
lines changed

15 files changed

+979
-4
lines changed

Doctrine/Orm/Filter/RangeFilter.php

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the DunglasApiBundle package.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Dunglas\ApiBundle\Doctrine\Orm\Filter;
13+
14+
use Doctrine\ORM\QueryBuilder;
15+
use Dunglas\ApiBundle\Api\ResourceInterface;
16+
use Dunglas\ApiBundle\Doctrine\Orm\Util\QueryNameGenerator;
17+
use Dunglas\ApiBundle\Exception\InvalidArgumentException;
18+
use Symfony\Component\HttpFoundation\Request;
19+
20+
/**
21+
* Filters the collection by range.
22+
*
23+
* @author Lee Siong Chan <[email protected]>
24+
*/
25+
class RangeFilter extends AbstractFilter
26+
{
27+
const PARAMETER_BETWEEN = 'between';
28+
const PARAMETER_GREATER_THAN = 'gt';
29+
const PARAMETER_GREATER_THAN_OR_EQUAL = 'gte';
30+
const PARAMETER_LESS_THAN = 'lt';
31+
const PARAMETER_LESS_THAN_OR_EQUAL = 'lte';
32+
33+
/**
34+
* {@inheritdoc}
35+
*/
36+
public function apply(ResourceInterface $resource, QueryBuilder $queryBuilder, Request $request)
37+
{
38+
foreach ($this->extractProperties($request) as $property => $values) {
39+
if (
40+
!is_array($values) ||
41+
!$this->isPropertyEnabled($property) ||
42+
!$this->isPropertyMapped($property, $resource)
43+
) {
44+
continue;
45+
}
46+
47+
$alias = 'o';
48+
$field = $property;
49+
50+
if ($this->isPropertyNested($property)) {
51+
$propertyParts = $this->splitPropertyParts($property);
52+
53+
$parentAlias = $alias;
54+
55+
foreach ($propertyParts['associations'] as $association) {
56+
$alias = QueryNameGenerator::generateJoinAlias($association);
57+
$queryBuilder->join(sprintf('%s.%s', $parentAlias, $association), $alias);
58+
$parentAlias = $alias;
59+
}
60+
61+
$field = $propertyParts['field'];
62+
}
63+
64+
foreach ($values as $operator => $value) {
65+
$this->addWhere(
66+
$queryBuilder,
67+
$alias,
68+
$field,
69+
$operator,
70+
$value
71+
);
72+
}
73+
}
74+
}
75+
76+
/**
77+
* Adds the where clause according to the operator.
78+
*
79+
* @param QueryBuilder $queryBuilder
80+
* @param string $alias
81+
* @param string $field
82+
* @param string $operator
83+
* @param string $value
84+
*/
85+
private function addWhere(QueryBuilder $queryBuilder, $alias, $field, $operator, $value)
86+
{
87+
$valueParameter = QueryNameGenerator::generateParameterName(sprintf('%s_%s', $field, $operator));
88+
89+
switch ($operator) {
90+
case self::PARAMETER_BETWEEN:
91+
$rangeValue = explode('..', $value);
92+
93+
if (2 !== count($rangeValue)) {
94+
throw new InvalidArgumentException(sprintf('Invalid format for [%s], expected to be <min>..<max>', $operator));
95+
}
96+
97+
return $queryBuilder
98+
->andWhere(sprintf('%1$s.%2$s BETWEEN :%3$s_1 AND :%3$s_2', $alias, $field, $valueParameter))
99+
->setParameter(sprintf('%s_1', $valueParameter), $rangeValue[0])
100+
->setParameter(sprintf('%s_2', $valueParameter), $rangeValue[1]);
101+
102+
case self::PARAMETER_GREATER_THAN:
103+
return $queryBuilder
104+
->andWhere(sprintf('%s.%s > :%s', $alias, $field, $valueParameter))
105+
->setParameter($valueParameter, $value);
106+
107+
case self::PARAMETER_GREATER_THAN_OR_EQUAL:
108+
return $queryBuilder
109+
->andWhere(sprintf('%s.%s >= :%s', $alias, $field, $valueParameter))
110+
->setParameter($valueParameter, $value);
111+
112+
case self::PARAMETER_LESS_THAN:
113+
return $queryBuilder
114+
->andWhere(sprintf('%s.%s < :%s', $alias, $field, $valueParameter))
115+
->setParameter($valueParameter, $value);
116+
117+
case self::PARAMETER_LESS_THAN_OR_EQUAL:
118+
return $queryBuilder
119+
->andWhere(sprintf('%s.%s <= :%s', $alias, $field, $valueParameter))
120+
->setParameter($valueParameter, $value);
121+
}
122+
}
123+
124+
/**
125+
* {@inheritdoc}
126+
*/
127+
public function getDescription(ResourceInterface $resource)
128+
{
129+
$description = [];
130+
131+
$properties = $this->properties;
132+
if (null === $properties) {
133+
$properties = array_fill_keys($this->getClassMetadata($resource)->getFieldNames(), null);
134+
}
135+
136+
foreach ($properties as $property => $operator) {
137+
if (!$this->isPropertyMapped($property, $resource)) {
138+
continue;
139+
}
140+
141+
$description += $this->getFilterDescription($property, self::PARAMETER_BETWEEN);
142+
$description += $this->getFilterDescription($property, self::PARAMETER_GREATER_THAN);
143+
$description += $this->getFilterDescription($property, self::PARAMETER_GREATER_THAN_OR_EQUAL);
144+
$description += $this->getFilterDescription($property, self::PARAMETER_LESS_THAN);
145+
$description += $this->getFilterDescription($property, self::PARAMETER_LESS_THAN_OR_EQUAL);
146+
}
147+
148+
return $description;
149+
}
150+
151+
/**
152+
* Gets filter description.
153+
*
154+
* @param string $fieldName
155+
* @param string $period
156+
*
157+
* @return array
158+
*/
159+
private function getFilterDescription($fieldName, $period)
160+
{
161+
return [
162+
sprintf('%s[%s]', $fieldName, $period) => [
163+
'property' => $fieldName,
164+
'type' => 'string',
165+
'required' => false,
166+
],
167+
];
168+
}
169+
}

Resources/config/doctrine_orm.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
<argument>%api.collection.filter_name.order%</argument>
4141
</service>
4242

43+
<service id="api.doctrine.orm.range_filter" class="Dunglas\ApiBundle\Doctrine\Orm\Filter\RangeFilter" public="false" abstract="true">
44+
<argument type="service" id="doctrine" />
45+
</service>
46+
4347
<service id="api.doctrine.orm.date_filter" class="Dunglas\ApiBundle\Doctrine\Orm\Filter\DateFilter" public="false" abstract="true">
4448
<argument type="service" id="doctrine" />
4549
</service>

Tests/Behat/TestBundle/Entity/Dummy.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ class Dummy
7272
* @Assert\DateTime
7373
*/
7474
public $dummyDate;
75+
/**
76+
* @var string A dummy price.
77+
*
78+
* @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
79+
*/
80+
public $dummyPrice;
7581
/**
7682
* @var RelatedDummy A related dummy.
7783
*
@@ -156,6 +162,18 @@ public function getDummyDate()
156162
return $this->dummyDate;
157163
}
158164

165+
public function setDummyPrice($dummyPrice)
166+
{
167+
$this->dummyPrice = $dummyPrice;
168+
169+
return $this;
170+
}
171+
172+
public function getDummyPrice()
173+
{
174+
return $this->dummyPrice;
175+
}
176+
159177
public function setJsonData($jsonData)
160178
{
161179
$this->jsonData = $jsonData;

Tests/DependencyInjection/DunglasApiExtensionTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ private function getContainerBuilderProphecy($withDoctrine = true)
197197
$containerBuilderProphecy->setDefinition('api.doctrine.orm.default_data_provider', $definitionArgument)->shouldBeCalled();
198198
$containerBuilderProphecy->setDefinition('api.doctrine.orm.search_filter', $definitionArgument)->shouldBeCalled();
199199
$containerBuilderProphecy->setDefinition('api.doctrine.orm.order_filter', $definitionArgument)->shouldBeCalled();
200+
$containerBuilderProphecy->setDefinition('api.doctrine.orm.range_filter', $definitionArgument)->shouldBeCalled();
200201
$containerBuilderProphecy->setDefinition('api.doctrine.orm.date_filter', $definitionArgument)->shouldBeCalled();
201202
$containerBuilderProphecy->setDefinition('api.mapping.loaders.doctrine_identifier', $definitionArgument)->shouldBeCalled();
202203
$containerBuilderProphecy->setDefinition('api.property_info.doctrine_extractor', $definitionArgument)->shouldBeCalled();

Tests/Doctrine/Orm/Filter/OrderFilterTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ public function testGetDescriptionDefaultFields()
139139
'type' => 'string',
140140
'required' => false,
141141
],
142+
'order[dummyPrice]' => [
143+
'property' => 'dummyPrice',
144+
'type' => 'string',
145+
'required' => false,
146+
],
142147
'order[jsonData]' => [
143148
'property' => 'jsonData',
144149
'type' => 'string',

0 commit comments

Comments
 (0)