• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 15133993414

20 May 2025 09:30AM UTC coverage: 26.313% (-1.2%) from 27.493%
15133993414

Pull #7161

github

web-flow
Merge e2c03d45f into 5459ba375
Pull Request #7161: fix(metadata): infer parameter string type from schema

0 of 2 new or added lines in 1 file covered. (0.0%)

11019 existing lines in 363 files now uncovered.

12898 of 49018 relevant lines covered (26.31%)

34.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.78
/src/Doctrine/Orm/Filter/DateFilter.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
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\Doctrine\Orm\Filter;
15

16
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
17
use ApiPlatform\Doctrine\Common\Filter\DateFilterTrait;
18
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
19
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
20
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
21
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
22
use ApiPlatform\Metadata\Operation;
23
use ApiPlatform\Metadata\Parameter;
24
use ApiPlatform\Metadata\QueryParameter;
25
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
26
use Doctrine\DBAL\Types\Type as DBALType;
27
use Doctrine\DBAL\Types\Types;
28
use Doctrine\ORM\Query\Expr\Join;
29
use Doctrine\ORM\QueryBuilder;
30

31
/**
32
 * The date filter allows to filter a collection by date intervals.
33
 *
34
 * Syntax: `?property[<after|before|strictly_after|strictly_before>]=value`.
35
 *
36
 * The value can take any date format supported by the [`\DateTime` constructor](https://www.php.net/manual/en/datetime.construct.php).
37
 *
38
 * The `after` and `before` filters will filter including the value whereas `strictly_after` and `strictly_before` will filter excluding the value.
39
 *
40
 * The date filter is able to deal with date properties having `null` values. Four behaviors are available at the property level of the filter:
41
 * - Use the default behavior of the DBMS: use `null` strategy
42
 * - Exclude items: use `ApiPlatform\Doctrine\Orm\Filter\DateFilter::EXCLUDE_NULL` (`exclude_null`) strategy
43
 * - Consider items as oldest: use `ApiPlatform\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_BEFORE` (`include_null_before`) strategy
44
 * - Consider items as youngest: use `ApiPlatform\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_AFTER` (`include_null_after`) strategy
45
 * - Always include items: use `ApiPlatform\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_BEFORE_AND_AFTER` (`include_null_before_and_after`) strategy
46
 *
47
 * <div data-code-selector>
48
 *
49
 * ```php
50
 * <?php
51
 * // api/src/Entity/Book.php
52
 * use ApiPlatform\Metadata\ApiFilter;
53
 * use ApiPlatform\Metadata\ApiResource;
54
 * use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
55
 *
56
 * #[ApiResource]
57
 * #[ApiFilter(DateFilter::class, properties: ['createdAt'])]
58
 * class Book
59
 * {
60
 *     // ...
61
 * }
62
 * ```
63
 *
64
 * ```yaml
65
 * # config/services.yaml
66
 * services:
67
 *     book.date_filter:
68
 *         parent: 'api_platform.doctrine.orm.date_filter'
69
 *         arguments: [ { createdAt: ~ } ]
70
 *         tags:  [ 'api_platform.filter' ]
71
 *         # The following are mandatory only if a _defaults section is defined with inverted values.
72
 *         # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section)
73
 *         autowire: false
74
 *         autoconfigure: false
75
 *         public: false
76
 *
77
 * # api/config/api_platform/resources.yaml
78
 * resources:
79
 *     App\Entity\Book:
80
 *         - operations:
81
 *               ApiPlatform\Metadata\GetCollection:
82
 *                   filters: ['book.date_filter']
83
 * ```
84
 *
85
 * ```xml
86
 * <?xml version="1.0" encoding="UTF-8" ?>
87
 * <!-- api/config/services.xml -->
88
 * <?xml version="1.0" encoding="UTF-8" ?>
89
 * <container
90
 *         xmlns="http://symfony.com/schema/dic/services"
91
 *         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
92
 *         xsi:schemaLocation="http://symfony.com/schema/dic/services
93
 *         https://symfony.com/schema/dic/services/services-1.0.xsd">
94
 *     <services>
95
 *         <service id="book.date_filter" parent="api_platform.doctrine.orm.date_filter">
96
 *             <argument type="collection">
97
 *                 <argument key="createdAt"/>
98
 *             </argument>
99
 *             <tag name="api_platform.filter"/>
100
 *         </service>
101
 *     </services>
102
 * </container>
103
 * <!-- api/config/api_platform/resources.xml -->
104
 * <resources
105
 *         xmlns="https://api-platform.com/schema/metadata/resources-3.0"
106
 *         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
107
 *         xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0
108
 *         https://api-platform.com/schema/metadata/resources-3.0.xsd">
109
 *     <resource class="App\Entity\Book">
110
 *         <operations>
111
 *             <operation class="ApiPlatform\Metadata\GetCollection">
112
 *                 <filters>
113
 *                     <filter>book.date_filter</filter>
114
 *                 </filters>
115
 *             </operation>
116
 *         </operations>
117
 *     </resource>
118
 * </resources>
119
 * ```
120
 *
121
 * </div>
122
 *
123
 * Given that the collection endpoint is `/books`, you can filter books by date with the following query: `/books?createdAt[after]=2018-03-19`.
124
 *
125
 * @author Kévin Dunglas <dunglas@gmail.com>
126
 * @author Théo FIDRY <theo.fidry@gmail.com>
127
 */
128
final class DateFilter extends AbstractFilter implements DateFilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
129
{
130
    use DateFilterTrait;
131

132
    public const DOCTRINE_DATE_TYPES = [
133
        Types::DATE_MUTABLE => true,
134
        Types::DATETIME_MUTABLE => true,
135
        Types::DATETIMETZ_MUTABLE => true,
136
        Types::TIME_MUTABLE => true,
137
        Types::DATE_IMMUTABLE => true,
138
        Types::DATETIME_IMMUTABLE => true,
139
        Types::DATETIMETZ_IMMUTABLE => true,
140
        Types::TIME_IMMUTABLE => true,
141
    ];
142

143
    /**
144
     * {@inheritdoc}
145
     */
146
    protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
147
    {
148
        // Expect $value to be an array having the period as keys and the date value as values
149
        if (
UNCOV
150
            !\is_array($value)
27✔
UNCOV
151
            || !$this->isPropertyEnabled($property, $resourceClass)
27✔
UNCOV
152
            || !$this->isPropertyMapped($property, $resourceClass)
27✔
UNCOV
153
            || !$this->isDateField($property, $resourceClass)
27✔
154
        ) {
UNCOV
155
            return;
10✔
156
        }
157

UNCOV
158
        $alias = $queryBuilder->getRootAliases()[0];
19✔
UNCOV
159
        $field = $property;
19✔
160

UNCOV
161
        if ($this->isPropertyNested($property, $resourceClass) && \count($value) > 0) {
19✔
162
            [$alias, $field] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass, Join::INNER_JOIN);
×
163
        }
164

UNCOV
165
        $nullManagement = $this->properties[$property] ?? null;
19✔
UNCOV
166
        $type = (string) $this->getDoctrineFieldType($property, $resourceClass);
19✔
167

UNCOV
168
        if (self::EXCLUDE_NULL === $nullManagement) {
19✔
UNCOV
169
            $queryBuilder->andWhere($queryBuilder->expr()->isNotNull(\sprintf('%s.%s', $alias, $field)));
3✔
170
        }
171

UNCOV
172
        if (isset($value[self::PARAMETER_BEFORE])) {
19✔
UNCOV
173
            $this->addWhere(
11✔
UNCOV
174
                $queryBuilder,
11✔
UNCOV
175
                $queryNameGenerator,
11✔
UNCOV
176
                $alias,
11✔
UNCOV
177
                $field,
11✔
UNCOV
178
                self::PARAMETER_BEFORE,
11✔
UNCOV
179
                $value[self::PARAMETER_BEFORE],
11✔
UNCOV
180
                $nullManagement,
11✔
UNCOV
181
                $type
11✔
UNCOV
182
            );
11✔
183
        }
184

UNCOV
185
        if (isset($value[self::PARAMETER_STRICTLY_BEFORE])) {
19✔
UNCOV
186
            $this->addWhere(
2✔
UNCOV
187
                $queryBuilder,
2✔
UNCOV
188
                $queryNameGenerator,
2✔
UNCOV
189
                $alias,
2✔
UNCOV
190
                $field,
2✔
UNCOV
191
                self::PARAMETER_STRICTLY_BEFORE,
2✔
UNCOV
192
                $value[self::PARAMETER_STRICTLY_BEFORE],
2✔
UNCOV
193
                $nullManagement,
2✔
UNCOV
194
                $type
2✔
UNCOV
195
            );
2✔
196
        }
197

UNCOV
198
        if (isset($value[self::PARAMETER_AFTER])) {
19✔
UNCOV
199
            $this->addWhere(
5✔
UNCOV
200
                $queryBuilder,
5✔
UNCOV
201
                $queryNameGenerator,
5✔
UNCOV
202
                $alias,
5✔
UNCOV
203
                $field,
5✔
UNCOV
204
                self::PARAMETER_AFTER,
5✔
UNCOV
205
                $value[self::PARAMETER_AFTER],
5✔
UNCOV
206
                $nullManagement,
5✔
UNCOV
207
                $type
5✔
UNCOV
208
            );
5✔
209
        }
210

UNCOV
211
        if (isset($value[self::PARAMETER_STRICTLY_AFTER])) {
19✔
UNCOV
212
            $this->addWhere(
3✔
UNCOV
213
                $queryBuilder,
3✔
UNCOV
214
                $queryNameGenerator,
3✔
UNCOV
215
                $alias,
3✔
UNCOV
216
                $field,
3✔
UNCOV
217
                self::PARAMETER_STRICTLY_AFTER,
3✔
UNCOV
218
                $value[self::PARAMETER_STRICTLY_AFTER],
3✔
UNCOV
219
                $nullManagement,
3✔
UNCOV
220
                $type
3✔
UNCOV
221
            );
3✔
222
        }
223
    }
224

225
    /**
226
     * Adds the where clause according to the chosen null management.
227
     */
228
    protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, string $operator, mixed $value, ?string $nullManagement = null, DBALType|string|null $type = null): void
229
    {
UNCOV
230
        $type = (string) $type;
19✔
UNCOV
231
        $value = $this->normalizeValue($value, $operator);
19✔
232

UNCOV
233
        if (null === $value) {
19✔
234
            return;
×
235
        }
236

237
        try {
UNCOV
238
            $value = !str_contains($type, '_immutable') ? new \DateTime($value) : new \DateTimeImmutable($value);
19✔
239
        } catch (\Exception) {
×
240
            // Silently ignore this filter if it can not be transformed to a \DateTime
241
            $this->logger->notice('Invalid filter ignored', [
×
242
                'exception' => new InvalidArgumentException(\sprintf('The field "%s" has a wrong date format. Use one accepted by the \DateTime constructor', $field)),
×
243
            ]);
×
244

245
            return;
×
246
        }
247

UNCOV
248
        $valueParameter = $queryNameGenerator->generateParameterName($field);
19✔
UNCOV
249
        $operatorValue = [
19✔
UNCOV
250
            self::PARAMETER_BEFORE => '<=',
19✔
UNCOV
251
            self::PARAMETER_STRICTLY_BEFORE => '<',
19✔
UNCOV
252
            self::PARAMETER_AFTER => '>=',
19✔
UNCOV
253
            self::PARAMETER_STRICTLY_AFTER => '>',
19✔
UNCOV
254
        ];
19✔
UNCOV
255
        $baseWhere = \sprintf('%s.%s %s :%s', $alias, $field, $operatorValue[$operator], $valueParameter);
19✔
256

UNCOV
257
        if (null === $nullManagement || self::EXCLUDE_NULL === $nullManagement) {
19✔
UNCOV
258
            $queryBuilder->andWhere($baseWhere);
14✔
259
        } elseif (
UNCOV
260
            (self::INCLUDE_NULL_BEFORE === $nullManagement && \in_array($operator, [self::PARAMETER_BEFORE, self::PARAMETER_STRICTLY_BEFORE], true))
5✔
UNCOV
261
            || (self::INCLUDE_NULL_AFTER === $nullManagement && \in_array($operator, [self::PARAMETER_AFTER, self::PARAMETER_STRICTLY_AFTER], true))
5✔
UNCOV
262
            || (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))
5✔
263
        ) {
UNCOV
264
            $queryBuilder->andWhere($queryBuilder->expr()->orX(
5✔
UNCOV
265
                $baseWhere,
5✔
UNCOV
266
                $queryBuilder->expr()->isNull(\sprintf('%s.%s', $alias, $field))
5✔
UNCOV
267
            ));
5✔
268
        } else {
269
            $queryBuilder->andWhere($queryBuilder->expr()->andX(
×
270
                $baseWhere,
×
271
                $queryBuilder->expr()->isNotNull(\sprintf('%s.%s', $alias, $field))
×
272
            ));
×
273
        }
274

UNCOV
275
        $queryBuilder->setParameter($valueParameter, $value, $type);
19✔
276
    }
277

278
    /**
279
     * @return array<string, string>
280
     */
281
    public function getSchema(Parameter $parameter): array
282
    {
UNCOV
283
        return ['type' => 'string', 'format' => 'date'];
4✔
284
    }
285

286
    public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
287
    {
UNCOV
288
        $in = $parameter instanceof QueryParameter ? 'query' : 'header';
3✔
UNCOV
289
        $key = $parameter->getKey();
3✔
290

UNCOV
291
        return [
3✔
UNCOV
292
            new OpenApiParameter(name: $key.'[after]', in: $in),
3✔
UNCOV
293
            new OpenApiParameter(name: $key.'[before]', in: $in),
3✔
UNCOV
294
            new OpenApiParameter(name: $key.'[strictly_after]', in: $in),
3✔
UNCOV
295
            new OpenApiParameter(name: $key.'[strictly_before]', in: $in),
3✔
UNCOV
296
        ];
3✔
297
    }
298
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc