Completed
Push — master ( d7e498...f5c969 )
by Kévin
03:42
created

DateFilter::addWhere()   D

Complexity

Conditions 9
Paths 8

Size

Total Lines 41
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 41
rs 4.909
c 0
b 0
f 0
cc 9
eloc 27
nc 8
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 DOCTRINE_DATE_TYPES = [
37
        'date' => true,
38
        'datetime' => true,
39
        'datetimetz' => true,
40
        'time' => true,
41
        'date_immutable' => true,
42
        'datetime_immutable' => true,
43
        'datetimetz_immutable' => true,
44
        'time_immutable' => true,
45
    ];
46
47
    /**
48
     * {@inheritdoc}
49
     */
50 View Code Duplication
    public function getDescription(string $resourceClass): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
51
    {
52
        $description = [];
53
54
        $properties = $this->properties;
55
        if (null === $properties) {
56
            $properties = array_fill_keys($this->getClassMetadata($resourceClass)->getFieldNames(), null);
57
        }
58
59
        foreach ($properties as $property => $nullManagement) {
60
            if (!$this->isPropertyMapped($property, $resourceClass) || !$this->isDateField($property, $resourceClass)) {
61
                continue;
62
            }
63
64
            $description += $this->getFilterDescription($property, self::PARAMETER_BEFORE);
65
            $description += $this->getFilterDescription($property, self::PARAMETER_STRICTLY_BEFORE);
66
            $description += $this->getFilterDescription($property, self::PARAMETER_AFTER);
67
            $description += $this->getFilterDescription($property, self::PARAMETER_STRICTLY_AFTER);
68
        }
69
70
        return $description;
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76
    protected function filterProperty(string $property, $values, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
77
    {
78
        // Expect $values to be an array having the period as keys and the date value as values
79 View Code Duplication
        if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
80
            !\is_array($values) ||
81
            !$this->isPropertyEnabled($property, $resourceClass) ||
82
            !$this->isPropertyMapped($property, $resourceClass) ||
83
            !$this->isDateField($property, $resourceClass)
84
        ) {
85
            return;
86
        }
87
88
        $alias = $queryBuilder->getRootAliases()[0];
89
        $field = $property;
90
91 View Code Duplication
        if ($this->isPropertyNested($property, $resourceClass)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
92
            list($alias, $field) = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass);
93
        }
94
95
        $nullManagement = $this->properties[$property] ?? null;
96
        $type = $this->getDoctrineFieldType($property, $resourceClass);
97
98
        if (self::EXCLUDE_NULL === $nullManagement) {
99
            $queryBuilder->andWhere($queryBuilder->expr()->isNotNull(sprintf('%s.%s', $alias, $field)));
100
        }
101
102 View Code Duplication
        if (isset($values[self::PARAMETER_BEFORE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
103
            $this->addWhere(
104
                $queryBuilder,
105
                $queryNameGenerator,
106
                $alias,
107
                $field,
108
                self::PARAMETER_BEFORE,
109
                $values[self::PARAMETER_BEFORE],
110
                $nullManagement,
111
                $type
112
            );
113
        }
114
115 View Code Duplication
        if (isset($values[self::PARAMETER_STRICTLY_BEFORE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
116
            $this->addWhere(
117
                $queryBuilder,
118
                $queryNameGenerator,
119
                $alias,
120
                $field,
121
                self::PARAMETER_STRICTLY_BEFORE,
122
                $values[self::PARAMETER_STRICTLY_BEFORE],
123
                $nullManagement,
124
                $type
125
            );
126
        }
127
128 View Code Duplication
        if (isset($values[self::PARAMETER_AFTER])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
129
            $this->addWhere(
130
                $queryBuilder,
131
                $queryNameGenerator,
132
                $alias,
133
                $field,
134
                self::PARAMETER_AFTER,
135
                $values[self::PARAMETER_AFTER],
136
                $nullManagement,
137
                $type
138
            );
139
        }
140
141 View Code Duplication
        if (isset($values[self::PARAMETER_STRICTLY_AFTER])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
142
            $this->addWhere(
143
                $queryBuilder,
144
                $queryNameGenerator,
145
                $alias,
146
                $field,
147
                self::PARAMETER_STRICTLY_AFTER,
148
                $values[self::PARAMETER_STRICTLY_AFTER],
149
                $nullManagement,
150
                $type
151
            );
152
        }
153
    }
154
155
    /**
156
     * Adds the where clause according to the chosen null management.
157
     *
158
     * @param QueryBuilder                $queryBuilder
159
     * @param QueryNameGeneratorInterface $queryNameGenerator
160
     * @param string                      $alias
161
     * @param string                      $field
162
     * @param string                      $operator
163
     * @param string                      $value
164
     * @param string|null                 $nullManagement
165
     * @param string|Type                 $type
166
     */
167
    protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, string $operator, string $value, string $nullManagement = null, $type = null)
168
    {
169
        try {
170
            $value = false !== strpos('_immutable', $type) ? new \DateTime($value) : new \DateTimeImmutable($value);
171
        } catch (\Exception $e) {
172
            // Silently ignore this filter if it can not be transformed to a \DateTime
173
            $this->logger->notice('Invalid filter ignored', [
174
              'exception' => new InvalidArgumentException(sprintf('The field "%s" has a wrong date format. Use one accepted by the \DateTime constructor', $field)),
175
          ]);
176
177
            return;
178
        }
179
180
        $valueParameter = $queryNameGenerator->generateParameterName($field);
181
        $operatorValue = [
182
            self::PARAMETER_BEFORE => '<=',
183
            self::PARAMETER_STRICTLY_BEFORE => '<',
184
            self::PARAMETER_AFTER => '>=',
185
            self::PARAMETER_STRICTLY_AFTER => '>',
186
        ];
187
        $baseWhere = sprintf('%s.%s %s :%s', $alias, $field, $operatorValue[$operator], $valueParameter);
188
189
        if (null === $nullManagement || self::EXCLUDE_NULL === $nullManagement) {
190
            $queryBuilder->andWhere($baseWhere);
191
        } elseif (
192
            (\in_array($operator, [self::PARAMETER_BEFORE, self::PARAMETER_STRICTLY_BEFORE], true) && self::INCLUDE_NULL_BEFORE === $nullManagement) ||
193
            (\in_array($operator, [self::PARAMETER_AFTER, self::PARAMETER_STRICTLY_AFTER], true) && self::INCLUDE_NULL_AFTER === $nullManagement)
194
        ) {
195
            $queryBuilder->andWhere($queryBuilder->expr()->orX(
196
                $baseWhere,
197
                $queryBuilder->expr()->isNull(sprintf('%s.%s', $alias, $field))
198
            ));
199
        } else {
200
            $queryBuilder->andWhere($queryBuilder->expr()->andX(
201
                $baseWhere,
202
                $queryBuilder->expr()->isNotNull(sprintf('%s.%s', $alias, $field))
203
            ));
204
        }
205
206
        $queryBuilder->setParameter($valueParameter, $value, $type);
0 ignored issues
show
Bug introduced by
It seems like $type defined by parameter $type on line 167 can also be of type object<Doctrine\DBAL\Types\Type>; however, Doctrine\ORM\QueryBuilder::setParameter() does only seem to accept string|integer|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
207
    }
208
209
    /**
210
     * Determines whether the given property refers to a date field.
211
     *
212
     * @param string $property
213
     * @param string $resourceClass
214
     *
215
     * @return bool
216
     */
217
    protected function isDateField(string $property, string $resourceClass): bool
218
    {
219
        return isset(self::DOCTRINE_DATE_TYPES[$this->getDoctrineFieldType($property, $resourceClass)]);
220
    }
221
222
    /**
223
     * Gets filter description.
224
     *
225
     * @param string $property
226
     * @param string $period
227
     *
228
     * @return array
229
     */
230
    protected function getFilterDescription(string $property, string $period): array
231
    {
232
        return [
233
            sprintf('%s[%s]', $property, $period) => [
234
                'property' => $property,
235
                'type' => \DateTimeInterface::class,
236
                'required' => false,
237
            ],
238
        ];
239
    }
240
}
241