1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the API Platform project. |
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
|
|
|
declare(strict_types=1); |
13
|
|
|
|
14
|
|
|
namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Filter; |
15
|
|
|
|
16
|
|
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; |
17
|
|
|
use ApiPlatform\Core\Exception\InvalidArgumentException; |
18
|
|
|
use Doctrine\DBAL\Types\Type; |
19
|
|
|
use Doctrine\ORM\QueryBuilder; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Filters the collection by date intervals. |
23
|
|
|
* |
24
|
|
|
* @author Kévin Dunglas <[email protected]> |
25
|
|
|
* @author Théo FIDRY <[email protected]> |
26
|
|
|
*/ |
27
|
|
|
class DateFilter extends AbstractContextAwareFilter |
28
|
|
|
{ |
29
|
|
|
const PARAMETER_BEFORE = 'before'; |
30
|
|
|
const PARAMETER_STRICTLY_BEFORE = 'strictly_before'; |
31
|
|
|
const PARAMETER_AFTER = 'after'; |
32
|
|
|
const PARAMETER_STRICTLY_AFTER = 'strictly_after'; |
33
|
|
|
const EXCLUDE_NULL = 'exclude_null'; |
34
|
|
|
const INCLUDE_NULL_BEFORE = 'include_null_before'; |
35
|
|
|
const INCLUDE_NULL_AFTER = 'include_null_after'; |
36
|
|
|
const INCLUDE_NULL_BEFORE_AND_AFTER = 'include_null_before_and_after'; |
37
|
|
|
const DOCTRINE_DATE_TYPES = [ |
38
|
|
|
'date' => true, |
39
|
|
|
'datetime' => true, |
40
|
|
|
'datetimetz' => true, |
41
|
|
|
'time' => true, |
42
|
|
|
'date_immutable' => true, |
43
|
|
|
'datetime_immutable' => true, |
44
|
|
|
'datetimetz_immutable' => true, |
45
|
|
|
'time_immutable' => true, |
46
|
|
|
]; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* {@inheritdoc} |
50
|
|
|
*/ |
51
|
|
|
public function getDescription(string $resourceClass): array |
52
|
|
|
{ |
53
|
|
|
$description = []; |
54
|
|
|
|
55
|
|
|
$properties = $this->properties; |
56
|
|
|
if (null === $properties) { |
57
|
|
|
$properties = array_fill_keys($this->getClassMetadata($resourceClass)->getFieldNames(), null); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
foreach ($properties as $property => $nullManagement) { |
61
|
|
|
if (!$this->isPropertyMapped($property, $resourceClass) || !$this->isDateField($property, $resourceClass)) { |
62
|
|
|
continue; |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
$description += $this->getFilterDescription($property, self::PARAMETER_BEFORE); |
66
|
|
|
$description += $this->getFilterDescription($property, self::PARAMETER_STRICTLY_BEFORE); |
67
|
|
|
$description += $this->getFilterDescription($property, self::PARAMETER_AFTER); |
68
|
|
|
$description += $this->getFilterDescription($property, self::PARAMETER_STRICTLY_AFTER); |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
return $description; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* {@inheritdoc} |
76
|
|
|
*/ |
77
|
|
|
protected function filterProperty(string $property, $values, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null) |
78
|
|
|
{ |
79
|
|
|
// Expect $values to be an array having the period as keys and the date value as values |
80
|
|
|
if ( |
81
|
|
|
!\is_array($values) || |
82
|
|
|
!$this->isPropertyEnabled($property, $resourceClass) || |
83
|
|
|
!$this->isPropertyMapped($property, $resourceClass) || |
84
|
|
|
!$this->isDateField($property, $resourceClass) |
85
|
|
|
) { |
86
|
|
|
return; |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
$alias = $queryBuilder->getRootAliases()[0]; |
90
|
|
|
$field = $property; |
91
|
|
|
|
92
|
|
|
if ($this->isPropertyNested($property, $resourceClass)) { |
93
|
|
|
list($alias, $field) = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
$nullManagement = $this->properties[$property] ?? null; |
97
|
|
|
$type = $this->getDoctrineFieldType($property, $resourceClass); |
98
|
|
|
|
99
|
|
|
if (self::EXCLUDE_NULL === $nullManagement) { |
100
|
|
|
$queryBuilder->andWhere($queryBuilder->expr()->isNotNull(sprintf('%s.%s', $alias, $field))); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
if (isset($values[self::PARAMETER_BEFORE])) { |
104
|
|
|
$this->addWhere( |
105
|
|
|
$queryBuilder, |
106
|
|
|
$queryNameGenerator, |
107
|
|
|
$alias, |
108
|
|
|
$field, |
109
|
|
|
self::PARAMETER_BEFORE, |
110
|
|
|
$values[self::PARAMETER_BEFORE], |
111
|
|
|
$nullManagement, |
112
|
|
|
$type |
113
|
|
|
); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
if (isset($values[self::PARAMETER_STRICTLY_BEFORE])) { |
117
|
|
|
$this->addWhere( |
118
|
|
|
$queryBuilder, |
119
|
|
|
$queryNameGenerator, |
120
|
|
|
$alias, |
121
|
|
|
$field, |
122
|
|
|
self::PARAMETER_STRICTLY_BEFORE, |
123
|
|
|
$values[self::PARAMETER_STRICTLY_BEFORE], |
124
|
|
|
$nullManagement, |
125
|
|
|
$type |
126
|
|
|
); |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
if (isset($values[self::PARAMETER_AFTER])) { |
130
|
|
|
$this->addWhere( |
131
|
|
|
$queryBuilder, |
132
|
|
|
$queryNameGenerator, |
133
|
|
|
$alias, |
134
|
|
|
$field, |
135
|
|
|
self::PARAMETER_AFTER, |
136
|
|
|
$values[self::PARAMETER_AFTER], |
137
|
|
|
$nullManagement, |
138
|
|
|
$type |
139
|
|
|
); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
if (isset($values[self::PARAMETER_STRICTLY_AFTER])) { |
143
|
|
|
$this->addWhere( |
144
|
|
|
$queryBuilder, |
145
|
|
|
$queryNameGenerator, |
146
|
|
|
$alias, |
147
|
|
|
$field, |
148
|
|
|
self::PARAMETER_STRICTLY_AFTER, |
149
|
|
|
$values[self::PARAMETER_STRICTLY_AFTER], |
150
|
|
|
$nullManagement, |
151
|
|
|
$type |
152
|
|
|
); |
153
|
|
|
} |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Adds the where clause according to the chosen null management. |
158
|
|
|
* |
159
|
|
|
* @param QueryBuilder $queryBuilder |
160
|
|
|
* @param QueryNameGeneratorInterface $queryNameGenerator |
161
|
|
|
* @param string $alias |
162
|
|
|
* @param string $field |
163
|
|
|
* @param string $operator |
164
|
|
|
* @param string $value |
165
|
|
|
* @param string|null $nullManagement |
166
|
|
|
* @param string|Type $type |
167
|
|
|
*/ |
168
|
|
|
protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, string $operator, string $value, string $nullManagement = null, $type = null) |
169
|
|
|
{ |
170
|
|
|
try { |
171
|
|
|
$value = false === strpos($type, '_immutable') ? new \DateTime($value) : new \DateTimeImmutable($value); |
172
|
|
|
} catch (\Exception $e) { |
173
|
|
|
// Silently ignore this filter if it can not be transformed to a \DateTime |
174
|
|
|
$this->logger->notice('Invalid filter ignored', [ |
175
|
|
|
'exception' => new InvalidArgumentException(sprintf('The field "%s" has a wrong date format. Use one accepted by the \DateTime constructor', $field)), |
176
|
|
|
]); |
177
|
|
|
|
178
|
|
|
return; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
$valueParameter = $queryNameGenerator->generateParameterName($field); |
182
|
|
|
$operatorValue = [ |
183
|
|
|
self::PARAMETER_BEFORE => '<=', |
184
|
|
|
self::PARAMETER_STRICTLY_BEFORE => '<', |
185
|
|
|
self::PARAMETER_AFTER => '>=', |
186
|
|
|
self::PARAMETER_STRICTLY_AFTER => '>', |
187
|
|
|
]; |
188
|
|
|
$baseWhere = sprintf('%s.%s %s :%s', $alias, $field, $operatorValue[$operator], $valueParameter); |
189
|
|
|
|
190
|
|
|
if (null === $nullManagement || self::EXCLUDE_NULL === $nullManagement) { |
191
|
|
|
$queryBuilder->andWhere($baseWhere); |
192
|
|
|
} elseif ( |
193
|
|
|
(self::INCLUDE_NULL_BEFORE === $nullManagement && \in_array($operator, [self::PARAMETER_BEFORE, self::PARAMETER_STRICTLY_BEFORE], true)) || |
194
|
|
|
(self::INCLUDE_NULL_AFTER === $nullManagement && \in_array($operator, [self::PARAMETER_AFTER, self::PARAMETER_STRICTLY_AFTER], true)) || |
195
|
|
|
(self::INCLUDE_NULL_BEFORE_AND_AFTER === $nullManagement && \in_array($operator, [self::PARAMETER_AFTER, self::PARAMETER_STRICTLY_AFTER, self::PARAMETER_BEFORE, self::PARAMETER_STRICTLY_BEFORE], true)) |
196
|
|
|
) { |
197
|
|
|
$queryBuilder->andWhere($queryBuilder->expr()->orX( |
198
|
|
|
$baseWhere, |
199
|
|
|
$queryBuilder->expr()->isNull(sprintf('%s.%s', $alias, $field)) |
200
|
|
|
)); |
201
|
|
|
} else { |
202
|
|
|
$queryBuilder->andWhere($queryBuilder->expr()->andX( |
203
|
|
|
$baseWhere, |
204
|
|
|
$queryBuilder->expr()->isNotNull(sprintf('%s.%s', $alias, $field)) |
205
|
|
|
)); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
$queryBuilder->setParameter($valueParameter, $value, $type); |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* Determines whether the given property refers to a date field. |
213
|
|
|
* |
214
|
|
|
* @param string $property |
215
|
|
|
* @param string $resourceClass |
216
|
|
|
* |
217
|
|
|
* @return bool |
218
|
|
|
*/ |
219
|
|
|
protected function isDateField(string $property, string $resourceClass): bool |
220
|
|
|
{ |
221
|
|
|
return isset(self::DOCTRINE_DATE_TYPES[$this->getDoctrineFieldType($property, $resourceClass)]); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Gets filter description. |
226
|
|
|
* |
227
|
|
|
* @param string $property |
228
|
|
|
* @param string $period |
229
|
|
|
* |
230
|
|
|
* @return array |
231
|
|
|
*/ |
232
|
|
|
protected function getFilterDescription(string $property, string $period): array |
233
|
|
|
{ |
234
|
|
|
return [ |
235
|
|
|
sprintf('%s[%s]', $property, $period) => [ |
236
|
|
|
'property' => $property, |
237
|
|
|
'type' => \DateTimeInterface::class, |
238
|
|
|
'required' => false, |
239
|
|
|
], |
240
|
|
|
]; |
241
|
|
|
} |
242
|
|
|
} |
243
|
|
|
|