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

api-platform / core / 13897184482

17 Mar 2025 10:29AM UTC coverage: 17.371% (+8.6%) from 8.768%
13897184482

Pull #7027

github

web-flow
Merge 6b496c593 into ba5cea729
Pull Request #7027: fix(laravel): json api default parameters

0 of 18 new or added lines in 2 files covered. (0.0%)

545 existing lines in 58 files now uncovered.

4729 of 27224 relevant lines covered (17.37%)

78.16 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 (
150
            !\is_array($value)
54✔
151
            || !$this->isPropertyEnabled($property, $resourceClass)
54✔
152
            || !$this->isPropertyMapped($property, $resourceClass)
54✔
153
            || !$this->isDateField($property, $resourceClass)
54✔
154
        ) {
155
            return;
20✔
156
        }
157

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

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

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

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

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

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

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

211
        if (isset($value[self::PARAMETER_STRICTLY_AFTER])) {
38✔
212
            $this->addWhere(
6✔
213
                $queryBuilder,
6✔
214
                $queryNameGenerator,
6✔
215
                $alias,
6✔
216
                $field,
6✔
217
                self::PARAMETER_STRICTLY_AFTER,
6✔
218
                $value[self::PARAMETER_STRICTLY_AFTER],
6✔
219
                $nullManagement,
6✔
220
                $type
6✔
221
            );
6✔
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
    {
230
        $type = (string) $type;
38✔
231
        $value = $this->normalizeValue($value, $operator);
38✔
232

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

237
        try {
238
            $value = !str_contains($type, '_immutable') ? new \DateTime($value) : new \DateTimeImmutable($value);
38✔
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

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

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

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

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

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

291
        return [
5✔
292
            new OpenApiParameter(name: $key.'[after]', in: $in),
5✔
293
            new OpenApiParameter(name: $key.'[before]', in: $in),
5✔
294
            new OpenApiParameter(name: $key.'[strictly_after]', in: $in),
5✔
295
            new OpenApiParameter(name: $key.'[strictly_before]', in: $in),
5✔
296
        ];
5✔
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