Completed
Push — master ( 26cfe1...df7087 )
by Han Hui
31s queued 11s
created

ExistsFilter   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 169
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 39
eloc 77
dl 0
loc 169
rs 9.28
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A isAssociationNullable() 0 18 6
D filterProperty() 0 63 19
A apply() 0 11 3
A isNullableField() 0 26 6
A __construct() 0 5 1
A extractProperties() 0 13 4
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\Common\Filter\ExistsFilterInterface;
17
use ApiPlatform\Core\Bridge\Doctrine\Common\Filter\ExistsFilterTrait;
18
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper;
19
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
20
use Doctrine\Common\Persistence\ManagerRegistry;
21
use Doctrine\ORM\Mapping\ClassMetadataInfo;
22
use Doctrine\ORM\Query\Expr\Join;
23
use Doctrine\ORM\QueryBuilder;
24
use Psr\Log\LoggerInterface;
25
use Symfony\Component\HttpFoundation\Request;
26
use Symfony\Component\HttpFoundation\RequestStack;
27
28
/**
29
 * Filters the collection by whether a property value exists or not.
30
 *
31
 * For each property passed, if the resource does not have such property or if
32
 * the value is not one of ( "true" | "false" | "1" | "0" ) the property is ignored.
33
 *
34
 * A query parameter with key but no value is treated as `true`, e.g.:
35
 * Request: GET /products?exists[brand]
36
 * Interpretation: filter products which have a brand
37
 *
38
 * @author Teoh Han Hui <[email protected]>
39
 */
40
class ExistsFilter extends AbstractContextAwareFilter implements ExistsFilterInterface
41
{
42
    use ExistsFilterTrait;
43
44
    public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $requestStack = null, LoggerInterface $logger = null, array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY)
45
    {
46
        parent::__construct($managerRegistry, $requestStack, $logger, $properties);
47
48
        $this->existsParameterName = $existsParameterName;
49
    }
50
51
    /**
52
     * {@inheritdoc}
53
     */
54
    public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = [])
55
    {
56
        if (!\is_array($context['filters'][$this->existsParameterName] ?? null)) {
57
            $context['exists_deprecated_syntax'] = true;
58
            parent::apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
59
60
            return;
61
        }
62
63
        foreach ($context['filters'][$this->existsParameterName] as $property => $value) {
64
            $this->filterProperty($property, $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
65
        }
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null/*, array $context = []*/)
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
72
    {
73
        if (\func_num_args() > 6) {
74
            $context = func_get_arg(6);
75
        } else {
76
            if (__CLASS__ !== \get_class($this)) {
77
                $r = new \ReflectionMethod($this, __FUNCTION__);
78
                if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
79
                    @trigger_error(sprintf('Method %s() will have a seventh `$context` argument in version API Platform 3.0. Not defining it is deprecated since API Platform 2.5.', __FUNCTION__), E_USER_DEPRECATED);
80
                }
81
            }
82
            $context = [];
83
        }
84
85
        if (
86
            (($context['exists_deprecated_syntax'] ?? false) && !isset($value[self::QUERY_PARAMETER_KEY])) ||
87
            !$this->isPropertyEnabled($property, $resourceClass) ||
88
            !$this->isPropertyMapped($property, $resourceClass, true) ||
89
            !$this->isNullableField($property, $resourceClass)
90
        ) {
91
            return;
92
        }
93
94
        $value = $this->normalizeValue($value, $property);
95
        if (null === $value) {
96
            return;
97
        }
98
99
        $alias = $queryBuilder->getRootAliases()[0];
100
        $field = $property;
101
102
        $associations = [];
103
        if ($this->isPropertyNested($property, $resourceClass)) {
104
            [$alias, $field, $associations] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass);
105
        }
106
        $metadata = $this->getNestedMetadata($resourceClass, $associations);
107
108
        if ($metadata->hasAssociation($field)) {
109
            if ($metadata->isCollectionValuedAssociation($field)) {
110
                $queryBuilder
111
                    ->andWhere(sprintf('%s.%s %s EMPTY', $alias, $field, $value ? 'IS NOT' : 'IS'));
112
113
                return;
114
            }
115
116
            if ($metadata->isAssociationInverseSide($field)) {
117
                $alias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, $alias, $field, Join::LEFT_JOIN);
118
119
                $queryBuilder
120
                    ->andWhere(sprintf('%s %s NULL', $alias, $value ? 'IS NOT' : 'IS'));
121
122
                return;
123
            }
124
125
            $queryBuilder
126
                ->andWhere(sprintf('%s.%s %s NULL', $alias, $field, $value ? 'IS NOT' : 'IS'));
127
128
            return;
129
        }
130
131
        if ($metadata->hasField($field)) {
132
            $queryBuilder
133
                ->andWhere(sprintf('%s.%s %s NULL', $alias, $field, $value ? 'IS NOT' : 'IS'));
134
        }
135
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140
    protected function isNullableField(string $property, string $resourceClass): bool
141
    {
142
        $propertyParts = $this->splitPropertyParts($property, $resourceClass);
143
        $metadata = $this->getNestedMetadata($resourceClass, $propertyParts['associations']);
144
145
        $field = $propertyParts['field'];
146
147
        if ($metadata->hasAssociation($field)) {
148
            if ($metadata->isSingleValuedAssociation($field)) {
149
                if (!($metadata instanceof ClassMetadataInfo)) {
150
                    return false;
151
                }
152
153
                $associationMapping = $metadata->getAssociationMapping($field);
154
155
                return $this->isAssociationNullable($associationMapping);
156
            }
157
158
            return true;
159
        }
160
161
        if ($metadata instanceof ClassMetadataInfo && $metadata->hasField($field)) {
162
            return $metadata->isNullable($field);
163
        }
164
165
        return false;
166
    }
167
168
    /**
169
     * Determines whether an association is nullable.
170
     *
171
     * @see https://github.com/doctrine/doctrine2/blob/v2.5.4/lib/Doctrine/ORM/Tools/EntityGenerator.php#L1221-L1246
172
     */
173
    private function isAssociationNullable(array $associationMapping): bool
174
    {
175
        if (!empty($associationMapping['id'])) {
176
            return false;
177
        }
178
179
        if (!isset($associationMapping['joinColumns'])) {
180
            return true;
181
        }
182
183
        $joinColumns = $associationMapping['joinColumns'];
184
        foreach ($joinColumns as $joinColumn) {
185
            if (isset($joinColumn['nullable']) && !$joinColumn['nullable']) {
186
                return false;
187
            }
188
        }
189
190
        return true;
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196
    protected function extractProperties(Request $request/*, string $resourceClass*/): array
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
197
    {
198
        if (!$request->query->has($this->existsParameterName)) {
199
            $resourceClass = \func_num_args() > 1 ? (string) func_get_arg(1) : null;
200
201
            return parent::extractProperties($request, $resourceClass);
202
        }
203
204
        @trigger_error(sprintf('The use of "%s::extractProperties()" is deprecated since 2.2. Use the "filters" key of the context instead.', __CLASS__), E_USER_DEPRECATED);
205
206
        $properties = $request->query->get($this->existsParameterName);
207
208
        return \is_array($properties) ? $properties : [];
209
    }
210
}
211