Skip to content

Commit 41ba7fa

Browse files
committed
3879: Implement at switch to date range filter, deprecate date filter
1 parent ed8e9dd commit 41ba7fa

File tree

8 files changed

+199
-29
lines changed

8 files changed

+199
-29
lines changed

src/Api/Dto/DailyOccurrence.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
use ApiPlatform\OpenApi\Model\Response;
1313
use App\Api\Filter\ElasticSearch\BooleanFilter;
1414
use App\Api\Filter\ElasticSearch\DateFilter;
15+
use App\Api\Filter\ElasticSearch\DateRangeFilter;
1516
use App\Api\Filter\ElasticSearch\IdFilter;
1617
use App\Api\Filter\ElasticSearch\MatchFilter;
1718
use App\Api\Filter\ElasticSearch\TagFilter;
1819
use App\Api\State\DailyOccurrenceRepresentationProvider;
19-
use App\Model\DateLimits;
20+
use App\Model\DateLimit;
2021

2122
#[ApiResource(
2223
operations: [
@@ -67,20 +68,21 @@
6768
properties: ['event.tags']
6869
)]
6970
#[ApiFilter(
70-
DateFilter::class,
71+
DateRangeFilter::class,
7172
properties: [
7273
'start' => 'start',
7374
'end' => 'end',
7475
'updated' => 'start',
7576
],
77+
// Arguments only exist to provide backward compatibility with filters originally defined by the DateFilter
7678
arguments: [
7779
'config' => [
7880
'start' => [
79-
'limit' => DateLimits::GreaterThanOrEqual,
81+
'limit' => DateLimit::gte,
8082
'throwOnInvalid' => true,
8183
],
8284
'end' => [
83-
'limit' => DateLimits::LessThanOrEqual,
85+
'limit' => DateLimit::lte,
8486
'throwOnInvalid' => true,
8587
],
8688
],

src/Api/Dto/Event.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
use ApiPlatform\OpenApi\Model\Response;
1313
use App\Api\Filter\ElasticSearch\BooleanFilter;
1414
use App\Api\Filter\ElasticSearch\DateFilter;
15+
use App\Api\Filter\ElasticSearch\DateRangeFilter;
1516
use App\Api\Filter\ElasticSearch\IdFilter;
1617
use App\Api\Filter\ElasticSearch\MatchFilter;
1718
use App\Api\Filter\ElasticSearch\TagFilter;
1819
use App\Api\State\EventRepresentationProvider;
19-
use App\Model\DateLimits;
20+
use App\Model\DateLimit;
2021

2122
#[ApiResource(
2223
operations: [
@@ -67,20 +68,21 @@
6768
properties: ['tags']
6869
)]
6970
#[ApiFilter(
70-
DateFilter::class,
71+
DateRangeFilter::class,
7172
properties: [
7273
'occurrences.start' => 'start',
7374
'occurrences.end' => 'end',
7475
'updated' => 'start',
7576
],
77+
// Arguments only exist to provide backward compatibility with filters originally defined by the DateFilter
7678
arguments: [
7779
'config' => [
7880
'start' => [
79-
'limit' => DateLimits::GreaterThanOrEqual,
81+
'limit' => DateLimit::gte,
8082
'throwOnInvalid' => true,
8183
],
8284
'end' => [
83-
'limit' => DateLimits::LessThanOrEqual,
85+
'limit' => DateLimit::lte,
8486
'throwOnInvalid' => true,
8587
],
8688
],

src/Api/Dto/Location.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
use ApiPlatform\OpenApi\Model\Parameter;
1212
use ApiPlatform\OpenApi\Model\Response;
1313
use App\Api\Filter\ElasticSearch\DateFilter;
14+
use App\Api\Filter\ElasticSearch\DateRangeFilter;
1415
use App\Api\Filter\ElasticSearch\MatchFilter;
1516
use App\Api\State\LocationRepresentationProvider;
16-
use App\Model\DateLimits;
17+
use App\Model\DateLimit;
1718

1819
#[ApiResource(
1920
operations: [
@@ -53,14 +54,15 @@
5354
properties: ['name', 'postalCode']
5455
)]
5556
#[ApiFilter(
56-
DateFilter::class,
57+
DateRangeFilter::class,
5758
properties: [
5859
'updated' => 'start',
5960
],
61+
// Arguments only exist to provide backward compatibility with filters originally defined by the DateFilter
6062
arguments: [
6163
'config' => [
6264
'start' => [
63-
'limit' => DateLimits::GreaterThanOrEqual,
65+
'limit' => DateLimit::gte,
6466
'throwOnInvalid' => true,
6567
],
6668
],

src/Api/Dto/Occurrence.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
use ApiPlatform\OpenApi\Model\Parameter;
1212
use ApiPlatform\OpenApi\Model\Response;
1313
use App\Api\Filter\ElasticSearch\DateFilter;
14+
use App\Api\Filter\ElasticSearch\DateRangeFilter;
1415
use App\Api\Filter\ElasticSearch\IdFilter;
1516
use App\Api\Filter\ElasticSearch\MatchFilter;
1617
use App\Api\Filter\ElasticSearch\TagFilter;
1718
use App\Api\State\OccurrenceRepresentationProvider;
18-
use App\Model\DateLimits;
19+
use App\Model\DateLimit;
1920

2021
#[ApiResource(
2122
operations: [
@@ -63,20 +64,21 @@
6364
properties: ['event.tags']
6465
)]
6566
#[ApiFilter(
66-
DateFilter::class,
67+
DateRangeFilter::class,
6768
properties: [
6869
'start' => 'start',
6970
'end' => 'end',
7071
'updated' => 'start',
7172
],
73+
// Arguments only exist to provide backward compatibility with filters originally defined by the DateFilter
7274
arguments: [
7375
'config' => [
7476
'start' => [
75-
'limit' => DateLimits::GreaterThanOrEqual,
77+
'limit' => DateLimit::gte,
7678
'throwOnInvalid' => true,
7779
],
7880
'end' => [
79-
'limit' => DateLimits::LessThanOrEqual,
81+
'limit' => DateLimit::lte,
8082
'throwOnInvalid' => true,
8183
],
8284
],

src/Api/Dto/Organization.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
use ApiPlatform\OpenApi\Model\Parameter;
1212
use ApiPlatform\OpenApi\Model\Response;
1313
use App\Api\Filter\ElasticSearch\DateFilter;
14+
use App\Api\Filter\ElasticSearch\DateRangeFilter;
1415
use App\Api\Filter\ElasticSearch\MatchFilter;
1516
use App\Api\State\OrganizationRepresentationProvider;
16-
use App\Model\DateLimits;
17+
use App\Model\DateLimit;
1718

1819
#[ApiResource(
1920
operations: [
@@ -53,14 +54,15 @@
5354
properties: ['name']
5455
)]
5556
#[ApiFilter(
56-
DateFilter::class,
57+
DateRangeFilter::class,
5758
properties: [
5859
'updated' => 'start',
5960
],
61+
// Arguments only exist to provide backward compatibility with filters originally defined by the DateFilter
6062
arguments: [
6163
'config' => [
6264
'start' => [
63-
'limit' => DateLimits::GreaterThanOrEqual,
65+
'limit' => DateLimit::gte,
6466
'throwOnInvalid' => true,
6567
],
6668
],

src/Api/Filter/ElasticSearch/DateFilter.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
/**
1515
* This class represents a filter that performs a search based on matching properties in a given resource.
16+
*
17+
* @deprecated please us DataRangeFilter
1618
*/
1719
final class DateFilter extends AbstractFilter
1820
{
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<?php
2+
3+
namespace App\Api\Filter\ElasticSearch;
4+
5+
use ApiPlatform\Elasticsearch\Filter\AbstractFilter;
6+
use ApiPlatform\Metadata\Operation;
7+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
8+
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
9+
use ApiPlatform\Metadata\ResourceClassResolverInterface;
10+
use App\Model\DateFilterConfig;
11+
use App\Model\DateLimit;
12+
use Symfony\Component\PropertyInfo\Type;
13+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
14+
15+
/**
16+
* DateRangeFilter allows for defining filters on datetime fields with operators e.g.
17+
* - startDate[gt]=2004-02-12T15:19:21+00:00
18+
* - startDate[between]=2004-02-12T15:19:21+00:00..2004-03-12T15:19:21+00:00.
19+
*
20+
* @see ApiPlatform\Doctrine\Orm\Filter\RangeFilter
21+
*/
22+
final class DateRangeFilter extends AbstractFilter
23+
{
24+
/** @var DateFilterConfig[] */
25+
private array $config;
26+
27+
public function __construct(
28+
protected PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory,
29+
PropertyMetadataFactoryInterface $propertyMetadataFactory,
30+
ResourceClassResolverInterface $resourceClassResolver,
31+
protected ?NameConverterInterface $nameConverter = null,
32+
protected ?array $properties = null,
33+
array $config = [],
34+
) {
35+
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceClassResolver, $nameConverter, $properties);
36+
37+
// Convert config to DateFilterConfig objects for better type safety.
38+
foreach ($config as $field => $values) {
39+
$this->config[$field] = new DateFilterConfig(
40+
$values['limit'],
41+
$values['throwOnInvalid']
42+
);
43+
}
44+
}
45+
46+
public function apply(array $clauseBody, string $resourceClass, ?Operation $operation = null, array $context = []): array
47+
{
48+
$ranges = [];
49+
50+
if (!$this->properties) {
51+
return $ranges;
52+
}
53+
54+
foreach ($this->properties as $property => $value) {
55+
if (!empty($context['filters'][$property])) {
56+
$ranges[] = $this->getElasticSearchQueryRanges($property, $context['filters'][$property]);
57+
}
58+
}
59+
60+
return isset($ranges[1]) ? $ranges : $ranges[0] ?? $ranges;
61+
}
62+
63+
public function getDescription(string $resourceClass): array
64+
{
65+
if (!$this->properties) {
66+
return [];
67+
}
68+
69+
$description = [];
70+
71+
foreach ($this->properties as $property => $value) {
72+
$description += $this->getFilterDescription($property, $this->config[$value]->limit, true);
73+
$description += $this->getFilterDescription($property, DateLimit::between);
74+
$description += $this->getFilterDescription($property, DateLimit::gt);
75+
$description += $this->getFilterDescription($property, DateLimit::gte);
76+
$description += $this->getFilterDescription($property, DateLimit::lt);
77+
$description += $this->getFilterDescription($property, DateLimit::lte);
78+
}
79+
80+
return $description;
81+
}
82+
83+
private function getElasticSearchQueryRanges($property, $filter): array
84+
{
85+
if (!\is_array($filter)) {
86+
$operator = $this->config[$property]->limit;
87+
$value = $filter;
88+
} else {
89+
$operator = DateLimit::{array_key_first($filter)};
90+
$value = array_shift($filter);
91+
}
92+
93+
switch ($operator) {
94+
case DateLimit::between:
95+
$values = explode('..', $value);
96+
97+
if (count($values) !== 2) {
98+
throw new \InvalidArgumentException('Invalid date range');
99+
}
100+
101+
return [
102+
'range' => [
103+
$property => [
104+
DateLimit::gt->name => $values[0],
105+
DateLimit::lt->name => $values[1],
106+
],
107+
],
108+
];
109+
case DateLimit::gt:
110+
case DateLimit::gte:
111+
case DateLimit::lt:
112+
case DateLimit::lte:
113+
return [
114+
'range' => [
115+
$property => [
116+
$operator->name => $value,
117+
],
118+
],
119+
];
120+
default:
121+
return [];
122+
}
123+
}
124+
125+
private function getFilterDescription(string $fieldName, DateLimit $operator, bool $isDefault = false): array
126+
{
127+
$propertyName = $this->normalizePropertyName($fieldName);
128+
$key = $this->getFilterDescriptionKey($propertyName, $operator, $isDefault);
129+
130+
return [
131+
$key => [
132+
'property' => $propertyName,
133+
'type' => 'string',
134+
'required' => false,
135+
'description' => $this->getFilterDescriptionBody($propertyName, $operator, $isDefault),
136+
],
137+
];
138+
}
139+
140+
private function getFilterDescriptionKey(string $propertyName, DateLimit $operator, bool $isDefault = false): string
141+
{
142+
return $isDefault ? $propertyName : $propertyName.'['.$operator->name.']';
143+
}
144+
145+
private function getFilterDescriptionBody(string $propertyName, DateLimit $operator, bool $isDeprecated = false): string
146+
{
147+
$deprecatedBody = $isDeprecated ? sprintf(' (DEPRECATED - please use a filter with an explicit operator, e.g. %s[gt]=2004-02-12T15:19:21+00:00) ', $propertyName) : '';
148+
149+
return match ($operator) {
150+
DateLimit::between => sprintf('Filter based on %s %s two ISO 8601 datetime (yyyy-MM-dd\'T\'HH:mm:ssz) seperated by \'..\', e.g. "2004-02-12T15:19:21+00:00..2004-02-13T16:20:22+00:00"', $propertyName, $operator->value),
151+
default => sprintf('Filter based on %s %s ISO 8601 datetime (yyyy-MM-dd\'T\'HH:mm:ssz), e.g. "2004-02-12T15:19:21+00:00"%s', $propertyName, $operator->value, $deprecatedBody),
152+
};
153+
}
154+
155+
private function normalizePropertyName(string $property): string
156+
{
157+
if (!$this->nameConverter instanceof NameConverterInterface) {
158+
return $property;
159+
}
160+
161+
return implode('.', array_map($this->nameConverter->normalize(...), explode('.', $property)));
162+
}
163+
}

src/Model/DateFilterConfig.php

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,25 @@
22

33
namespace App\Model;
44

5-
class DateFilterConfig
5+
readonly class DateFilterConfig
66
{
77
public function __construct(
8-
public readonly DateLimits $limit,
9-
public readonly bool $throwOnInvalid,
8+
public DateLimit $limit,
9+
public bool $throwOnInvalid,
1010
) {
1111
}
1212

1313
/**
1414
* Returns the comparison operator based on the given DateLimits.
1515
*
16-
* @param DateLimits $dateLimit
16+
* @param DateLimit $dateLimit
1717
* The DateLimit Enum to determine the comparison operator
1818
*
1919
* @return string
2020
* The comparison operator
2121
*/
22-
public function getCompareOperator(DateLimits $dateLimit): string
22+
public function getCompareOperator(DateLimit $dateLimit): string
2323
{
24-
return match ($dateLimit) {
25-
DateLimits::GreaterThanOrEqual => 'gte',
26-
DateLimits::GreaterThan => 'gt',
27-
DateLimits::LessThan => 'lt',
28-
DateLimits::LessThanOrEqual => 'lte',
29-
};
24+
return $dateLimit->name;
3025
}
3126
}

0 commit comments

Comments
 (0)