• 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

81.58
/src/Doctrine/Odm/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\Odm\Filter;
15

16
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
17
use ApiPlatform\Doctrine\Common\Filter\DateFilterTrait;
18
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
19
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
20
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
21
use ApiPlatform\Metadata\Operation;
22
use ApiPlatform\Metadata\Parameter;
23
use ApiPlatform\Metadata\QueryParameter;
24
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
25
use Doctrine\ODM\MongoDB\Aggregation\Builder;
26
use Doctrine\ODM\MongoDB\Types\Type as MongoDbType;
27

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

129
    public const DOCTRINE_DATE_TYPES = [
130
        MongoDbType::DATE => true,
131
        MongoDbType::DATE_IMMUTABLE => true,
132
    ];
133

134
    /**
135
     * {@inheritdoc}
136
     */
137
    protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
138
    {
139
        // Expect $value to be an array having the period as keys and the date value as values
140
        if (
141
            !\is_array($value)
117✔
142
            || !$this->isPropertyEnabled($property, $resourceClass)
117✔
143
            || !$this->isPropertyMapped($property, $resourceClass)
117✔
144
            || !$this->isDateField($property, $resourceClass)
117✔
145
        ) {
146
            return;
108✔
147
        }
148

149
        $matchField = $property;
15✔
150

151
        if ($this->isPropertyNested($property, $resourceClass)) {
15✔
152
            [$matchField] = $this->addLookupsForNestedProperty($property, $aggregationBuilder, $resourceClass);
3✔
153
        }
154

155
        $nullManagement = $this->properties[$property] ?? null;
15✔
156

157
        if (self::EXCLUDE_NULL === $nullManagement) {
15✔
158
            $aggregationBuilder->match()->field($matchField)->notEqual(null);
×
159
        }
160

161
        if (isset($value[self::PARAMETER_BEFORE])) {
15✔
162
            $this->addMatch(
6✔
163
                $aggregationBuilder,
6✔
164
                $matchField,
6✔
165
                self::PARAMETER_BEFORE,
6✔
166
                $value[self::PARAMETER_BEFORE],
6✔
167
                $nullManagement
6✔
168
            );
6✔
169
        }
170

171
        if (isset($value[self::PARAMETER_STRICTLY_BEFORE])) {
15✔
172
            $this->addMatch(
×
173
                $aggregationBuilder,
×
174
                $matchField,
×
175
                self::PARAMETER_STRICTLY_BEFORE,
×
176
                $value[self::PARAMETER_STRICTLY_BEFORE],
×
177
                $nullManagement
×
178
            );
×
179
        }
180

181
        if (isset($value[self::PARAMETER_AFTER])) {
15✔
182
            $this->addMatch(
14✔
183
                $aggregationBuilder,
14✔
184
                $matchField,
14✔
185
                self::PARAMETER_AFTER,
14✔
186
                $value[self::PARAMETER_AFTER],
14✔
187
                $nullManagement
14✔
188
            );
14✔
189
        }
190

191
        if (isset($value[self::PARAMETER_STRICTLY_AFTER])) {
15✔
192
            $this->addMatch(
1✔
193
                $aggregationBuilder,
1✔
194
                $matchField,
1✔
195
                self::PARAMETER_STRICTLY_AFTER,
1✔
196
                $value[self::PARAMETER_STRICTLY_AFTER],
1✔
197
                $nullManagement
1✔
198
            );
1✔
199
        }
200
    }
201

202
    /**
203
     * Adds the match stage according to the chosen null management.
204
     */
205
    private function addMatch(Builder $aggregationBuilder, string $field, string $operator, $value, ?string $nullManagement = null): void
206
    {
207
        $value = $this->normalizeValue($value, $operator);
15✔
208

209
        if (null === $value) {
15✔
210
            return;
×
211
        }
212

213
        try {
214
            $value = new \DateTime($value);
15✔
215
        } catch (\Exception) {
×
216
            // Silently ignore this filter if it can not be transformed to a \DateTime
217
            $this->logger->notice('Invalid filter ignored', [
×
218
                'exception' => new InvalidArgumentException(\sprintf('The field "%s" has a wrong date format. Use one accepted by the \DateTime constructor', $field)),
×
219
            ]);
×
220

221
            return;
×
222
        }
223

224
        $operatorValue = [
15✔
225
            self::PARAMETER_BEFORE => '$lte',
15✔
226
            self::PARAMETER_STRICTLY_BEFORE => '$lt',
15✔
227
            self::PARAMETER_AFTER => '$gte',
15✔
228
            self::PARAMETER_STRICTLY_AFTER => '$gt',
15✔
229
        ];
15✔
230

231
        if ((self::INCLUDE_NULL_BEFORE === $nullManagement && \in_array($operator, [self::PARAMETER_BEFORE, self::PARAMETER_STRICTLY_BEFORE], true))
15✔
232
            || (self::INCLUDE_NULL_AFTER === $nullManagement && \in_array($operator, [self::PARAMETER_AFTER, self::PARAMETER_STRICTLY_AFTER], true))
15✔
233
            || (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))
15✔
234
        ) {
235
            $aggregationBuilder->match()->addOr(
3✔
236
                $aggregationBuilder->matchExpr()->field($field)->operator($operatorValue[$operator], $value),
3✔
237
                $aggregationBuilder->matchExpr()->field($field)->equals(null)
3✔
238
            );
3✔
239

240
            return;
3✔
241
        }
242

243
        $aggregationBuilder->match()->addAnd($aggregationBuilder->matchExpr()->field($field)->operator($operatorValue[$operator], $value));
14✔
244
    }
245

246
    /**
247
     * @return array<string, string>
248
     */
249
    public function getSchema(Parameter $parameter): array
250
    {
UNCOV
251
        return ['type' => 'string', 'format' => 'date'];
2✔
252
    }
253

254
    public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
255
    {
UNCOV
256
        $in = $parameter instanceof QueryParameter ? 'query' : 'header';
2✔
UNCOV
257
        $key = $parameter->getKey();
2✔
258

UNCOV
259
        return [
2✔
UNCOV
260
            new OpenApiParameter(name: $key.'[after]', in: $in),
2✔
UNCOV
261
            new OpenApiParameter(name: $key.'[before]', in: $in),
2✔
UNCOV
262
            new OpenApiParameter(name: $key.'[strictly_after]', in: $in),
2✔
UNCOV
263
            new OpenApiParameter(name: $key.'[strictly_before]', in: $in),
2✔
UNCOV
264
        ];
2✔
265
    }
266
}
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