Completed
Push — master ( 04aca3...4b3d1a )
by
unknown
03:17
created

AbstractFilter::extractProperties()   C

Complexity

Conditions 11
Paths 20

Size

Total Lines 34
Code Lines 19

Duplication

Lines 11
Ratio 32.35 %

Importance

Changes 0
Metric Value
dl 11
loc 34
rs 5.2653
c 0
b 0
f 0
cc 11
eloc 19
nc 20
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\QueryChecker;
17
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
18
use ApiPlatform\Core\Exception\InvalidArgumentException;
19
use ApiPlatform\Core\Util\RequestParser;
20
use Doctrine\Common\Persistence\ManagerRegistry;
21
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
22
use Doctrine\ORM\Query\Expr\Join;
23
use Doctrine\ORM\QueryBuilder;
24
use Psr\Log\LoggerInterface;
25
use Psr\Log\NullLogger;
26
use Symfony\Component\HttpFoundation\Request;
27
use Symfony\Component\HttpFoundation\RequestStack;
28
29
/**
30
 * {@inheritdoc}
31
 *
32
 * Abstract class with helpers for easing the implementation of a filter.
33
 *
34
 * @author Kévin Dunglas <[email protected]>
35
 * @author Théo FIDRY <[email protected]>
36
 */
37
abstract class AbstractFilter implements FilterInterface
38
{
39
    protected $managerRegistry;
40
    protected $requestStack;
41
    protected $logger;
42
    protected $properties;
43
44
    public function __construct(ManagerRegistry $managerRegistry, RequestStack $requestStack, LoggerInterface $logger = null, array $properties = null)
45
    {
46
        $this->managerRegistry = $managerRegistry;
47
        $this->requestStack = $requestStack;
48
        $this->logger = $logger ?? new NullLogger();
49
        $this->properties = $properties;
50
    }
51
52
    /**
53
     * {@inheritdoc}
54
     */
55
    public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
56
    {
57
        $request = $this->requestStack->getCurrentRequest();
58
        if (null === $request) {
59
            return;
60
        }
61
62
        foreach ($this->extractProperties($request, $resourceClass) as $property => $value) {
63
            $this->filterProperty($property, $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName);
64
        }
65
    }
66
67
    /**
68
     * Passes a property through the filter.
69
     *
70
     * @param string                      $property
71
     * @param mixed                       $value
72
     * @param QueryBuilder                $queryBuilder
73
     * @param QueryNameGeneratorInterface $queryNameGenerator
74
     * @param string                      $resourceClass
75
     * @param string|null                 $operationName
76
     */
77
    abstract protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null);
78
79
    /**
80
     * Gets class metadata for the given resource.
81
     *
82
     * @param string $resourceClass
83
     *
84
     * @return ClassMetadata
85
     */
86
    protected function getClassMetadata(string $resourceClass): ClassMetadata
87
    {
88
        return $this
89
            ->managerRegistry
90
            ->getManagerForClass($resourceClass)
91
            ->getClassMetadata($resourceClass);
92
    }
93
94
    /**
95
     * Determines whether the given property is enabled.
96
     *
97
     * @param string $property
98
     *
99
     * @return bool
100
     */
101
    protected function isPropertyEnabled(string $property/*, string $resourceClass*/): bool
102
    {
103 View Code Duplication
        if (func_num_args() > 1) {
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...
104
            $resourceClass = func_get_arg(1);
105
        } else {
106
            if (__CLASS__ !== get_class($this)) {
107
                $r = new \ReflectionMethod($this, __FUNCTION__);
108
                if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
0 ignored issues
show
introduced by
Consider using $r->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
109
                    @trigger_error(sprintf('Method %s() will have a second `$resourceClass` argument in version API Platform 3.0. Not defining it is deprecated since API Platform 2.1.', __FUNCTION__), E_USER_DEPRECATED);
110
                }
111
            }
112
            $resourceClass = null;
113
        }
114
115
        if (null === $this->properties) {
116
            // to ensure sanity, nested properties must still be explicitly enabled
117
            return !$this->isPropertyNested($property, $resourceClass);
118
        }
119
120
        return array_key_exists($property, $this->properties);
121
    }
122
123
    /**
124
     * Determines whether the given property is mapped.
125
     *
126
     * @param string $property
127
     * @param string $resourceClass
128
     * @param bool   $allowAssociation
129
     *
130
     * @return bool
131
     */
132
    protected function isPropertyMapped(string $property, string $resourceClass, bool $allowAssociation = false): bool
133
    {
134 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...
135
            $propertyParts = $this->splitPropertyParts($property, $resourceClass);
136
            $metadata = $this->getNestedMetadata($resourceClass, $propertyParts['associations']);
137
            $property = $propertyParts['field'];
138
        } else {
139
            $metadata = $this->getClassMetadata($resourceClass);
140
        }
141
142
        return $metadata->hasField($property) || ($allowAssociation && $metadata->hasAssociation($property));
143
    }
144
145
    /**
146
     * Determines whether the given property is nested.
147
     *
148
     * @param string $property
149
     *
150
     * @return bool
151
     */
152
    protected function isPropertyNested(string $property/*, string $resourceClass*/): bool
153
    {
154 View Code Duplication
        if (func_num_args() > 1) {
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...
155
            $resourceClass = func_get_arg(1);
156
        } else {
157
            if (__CLASS__ !== get_class($this)) {
158
                $r = new \ReflectionMethod($this, __FUNCTION__);
159
                if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
0 ignored issues
show
introduced by
Consider using $r->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
160
                    @trigger_error(sprintf('Method %s() will have a second `$resourceClass` argument in version API Platform 3.0. Not defining it is deprecated since API Platform 2.1.', __FUNCTION__), E_USER_DEPRECATED);
161
                }
162
            }
163
            $resourceClass = null;
164
        }
165
166
        if (false === $pos = strpos($property, '.')) {
167
            return false;
168
        }
169
170
        return null !== $resourceClass && $this->getClassMetadata($resourceClass)->hasAssociation(substr($property, 0, $pos));
171
    }
172
173
    /**
174
     * Determines whether the given property is embedded.
175
     *
176
     * @param string $property
177
     * @param string $resourceClass
178
     *
179
     * @return bool
180
     */
181
    protected function isPropertyEmbedded(string $property, string $resourceClass): bool
182
    {
183
        return false !== strpos($property, '.') && $this->getClassMetadata($resourceClass)->hasField($property);
184
    }
185
186
    /**
187
     * Gets nested class metadata for the given resource.
188
     *
189
     * @param string   $resourceClass
190
     * @param string[] $associations
191
     *
192
     * @return ClassMetadata
193
     */
194
    protected function getNestedMetadata(string $resourceClass, array $associations): ClassMetadata
195
    {
196
        $metadata = $this->getClassMetadata($resourceClass);
197
198
        foreach ($associations as $association) {
199
            if ($metadata->hasAssociation($association)) {
200
                $associationClass = $metadata->getAssociationTargetClass($association);
201
202
                $metadata = $this
203
                    ->managerRegistry
204
                    ->getManagerForClass($associationClass)
205
                    ->getClassMetadata($associationClass);
206
            }
207
        }
208
209
        return $metadata;
210
    }
211
212
    /**
213
     * Splits the given property into parts.
214
     *
215
     * Returns an array with the following keys:
216
     *   - associations: array of associations according to nesting order
217
     *   - field: string holding the actual field (leaf node)
218
     *
219
     * @param string $property
220
     *
221
     * @return array
222
     */
223
    protected function splitPropertyParts(string $property/*, string $resourceClass*/): array
224
    {
225
        $parts = explode('.', $property);
226
227 View Code Duplication
        if (func_num_args() > 1) {
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...
228
            $resourceClass = func_get_arg(1);
229
        } else {
230
            if (__CLASS__ !== get_class($this)) {
231
                $r = new \ReflectionMethod($this, __FUNCTION__);
232
                if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
0 ignored issues
show
introduced by
Consider using $r->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
233
                    @trigger_error(sprintf('Method %s() will have a second `$resourceClass` argument in version API Platform 3.0. Not defining it is deprecated since API Platform 2.1.', __FUNCTION__), E_USER_DEPRECATED);
234
                }
235
            }
236
        }
237
238
        if (!isset($resourceClass)) {
239
            return [
240
                'associations' => array_slice($parts, 0, -1),
241
                'field' => end($parts),
242
            ];
243
        }
244
245
        $metadata = $this->getClassMetadata($resourceClass);
246
        $slice = 0;
247
248
        foreach ($parts as $part) {
249
            if ($metadata->hasAssociation($part)) {
250
                $metadata = $this->getClassMetadata($metadata->getAssociationTargetClass($part));
251
                $slice += 1;
252
            }
253
        }
254
255
        if ($slice === count($parts)) {
256
            $slice -= 1;
257
        }
258
259
        return [
260
            'associations' => array_slice($parts, 0, $slice),
261
            'field' => implode('.', array_slice($parts, $slice)),
262
        ];
263
    }
264
265
    /**
266
     * Extracts properties to filter from the request.
267
     *
268
     * @param Request $request
269
     *
270
     * @return array
271
     */
272
    protected function extractProperties(Request $request/*, string $resourceClass*/): array
273
    {
274 View Code Duplication
        if (func_num_args() > 1) {
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...
275
            $resourceClass = func_get_arg(1);
276
        } else {
277
            if (__CLASS__ !== get_class($this)) {
278
                $r = new \ReflectionMethod($this, __FUNCTION__);
279
                if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
0 ignored issues
show
introduced by
Consider using $r->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
280
                    @trigger_error(sprintf('Method %s() will have a second `$resourceClass` argument in version API Platform 3.0. Not defining it is deprecated since API Platform 2.1.', __FUNCTION__), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
281
                }
282
            }
283
            $resourceClass = null;
284
        }
285
286
        if (null !== $properties = $request->attributes->get('_api_filter_common')) {
287
            return $properties;
288
        }
289
290
        $needsFixing = false;
291
292
        if (null !== $this->properties) {
293
            foreach ($this->properties as $property => $value) {
294
                if (($this->isPropertyNested($property, $resourceClass) || $this->isPropertyEmbedded($property, $resourceClass)) && $request->query->has(str_replace('.', '_', $property))) {
295
                    $needsFixing = true;
296
                }
297
            }
298
        }
299
300
        if ($needsFixing) {
301
            $request = RequestParser::parseAndDuplicateRequest($request);
302
        }
303
304
        return $request->query->all();
305
    }
306
307
    /**
308
     * Adds the necessary joins for a nested property.
309
     *
310
     * @param string                      $property
311
     * @param string                      $rootAlias
312
     * @param QueryBuilder                $queryBuilder
313
     * @param QueryNameGeneratorInterface $queryNameGenerator
314
     *
315
     * @throws InvalidArgumentException If property is not nested
316
     *
317
     * @return array An array where the first element is the join $alias of the leaf entity,
318
     *               the second element is the $field name
319
     *               the third element is the $associations array
320
     */
321
    protected function addJoinsForNestedProperty(string $property, string $rootAlias, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator/*, string $resourceClass*/): array
322
    {
323 View Code Duplication
        if (func_num_args() > 4) {
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...
324
            $resourceClass = func_get_arg(4);
325
        } else {
326
            if (__CLASS__ !== get_class($this)) {
327
                $r = new \ReflectionMethod($this, __FUNCTION__);
328
                if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
0 ignored issues
show
introduced by
Consider using $r->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
329
                    @trigger_error(sprintf('Method %s() will have a fifth `$resourceClass` argument in version API Platform 3.0. Not defining it is deprecated since API Platform 2.1.', __FUNCTION__), E_USER_DEPRECATED);
330
                }
331
            }
332
            $resourceClass = null;
333
        }
334
335
        $propertyParts = $this->splitPropertyParts($property, $resourceClass);
336
        $parentAlias = $rootAlias;
337
338
        foreach ($propertyParts['associations'] as $association) {
339
            $alias = $this->addJoinOnce($queryBuilder, $queryNameGenerator, $parentAlias, $association);
340
            $parentAlias = $alias;
341
        }
342
343
        if (!isset($alias)) {
344
            throw new InvalidArgumentException(sprintf('Cannot add joins for property "%s" - property is not nested.', $property));
345
        }
346
347
        return [$alias, $propertyParts['field'], $propertyParts['associations']];
348
    }
349
350
    /**
351
     * Get the existing join from queryBuilder DQL parts.
352
     *
353
     * @param QueryBuilder $queryBuilder
354
     * @param string       $alias
355
     * @param string       $association  the association field
356
     *
357
     * @return Join|null
358
     */
359
    private function getExistingJoin(QueryBuilder $queryBuilder, string $alias, string $association)
360
    {
361
        $parts = $queryBuilder->getDQLPart('join');
362
363
        if (!isset($parts['o'])) {
364
            return null;
365
        }
366
367
        foreach ($parts['o'] as $join) {
368
            if (sprintf('%s.%s', $alias, $association) === $join->getJoin()) {
369
                return $join;
370
            }
371
        }
372
373
        return null;
374
    }
375
376
    /**
377
     * Adds a join to the queryBuilder if none exists.
378
     *
379
     * @param QueryBuilder                $queryBuilder
380
     * @param QueryNameGeneratorInterface $queryNameGenerator
381
     * @param string                      $alias
382
     * @param string                      $association        the association field
383
     *
384
     * @return string the new association alias
385
     */
386
    protected function addJoinOnce(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $association): string
387
    {
388
        $join = $this->getExistingJoin($queryBuilder, $alias, $association);
389
390
        if (null === $join) {
391
            $associationAlias = $queryNameGenerator->generateJoinAlias($association);
392
393
            if (true === QueryChecker::hasLeftJoin($queryBuilder)) {
394
                $queryBuilder
395
                    ->leftJoin(sprintf('%s.%s', $alias, $association), $associationAlias);
396
            } else {
397
                $queryBuilder
398
                    ->innerJoin(sprintf('%s.%s', $alias, $association), $associationAlias);
399
            }
400
        } else {
401
            $associationAlias = $join->getAlias();
402
        }
403
404
        return $associationAlias;
405
    }
406
}
407