Passed
Push — master ( b3bd65...19c08e )
by Alan
03:15
created

ExistsFilter::apply()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 5
dl 0
loc 11
rs 10
c 0
b 0
f 0
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
    /**
45
     * @param RequestStack|null $requestStack No prefix to prevent autowiring of this deprecated property
46
     */
47
    public function __construct(ManagerRegistry $managerRegistry, $requestStack = null, LoggerInterface $logger = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, array $properties = null)
48
    {
49
        parent::__construct($managerRegistry, $requestStack, $logger, $properties);
50
51
        $this->existsParameterName = $existsParameterName;
52
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57
    public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = [])
58
    {
59
        if (!\is_array($context['filters'][$this->existsParameterName] ?? null)) {
60
            $context['exists_deprecated_syntax'] = true;
61
            parent::apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
62
63
            return;
64
        }
65
66
        foreach ($context['filters'][$this->existsParameterName] as $property => $value) {
67
            $this->filterProperty($property, $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
68
        }
69
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74
    protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = []): void
75
    {
76
        if (
77
            (($context['exists_deprecated_syntax'] ?? false) && !isset($value[self::QUERY_PARAMETER_KEY])) ||
78
            !$this->isPropertyEnabled($property, $resourceClass) ||
79
            !$this->isPropertyMapped($property, $resourceClass, true) ||
80
            !$this->isNullableField($property, $resourceClass)
81
        ) {
82
            return;
83
        }
84
85
        $value = $this->normalizeValue($value, $property);
86
        if (null === $value) {
87
            return;
88
        }
89
90
        $alias = $queryBuilder->getRootAliases()[0];
91
        $field = $property;
92
93
        $associations = [];
94
        if ($this->isPropertyNested($property, $resourceClass)) {
95
            [$alias, $field, $associations] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass);
96
        }
97
        $metadata = $this->getNestedMetadata($resourceClass, $associations);
98
99
        if ($metadata->hasAssociation($field)) {
100
            if ($metadata->isCollectionValuedAssociation($field)) {
101
                $queryBuilder
102
                    ->andWhere(sprintf('%s.%s %s EMPTY', $alias, $field, $value ? 'IS NOT' : 'IS'));
103
104
                return;
105
            }
106
107
            if ($metadata->isAssociationInverseSide($field)) {
108
                $alias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, $alias, $field, Join::LEFT_JOIN);
109
110
                $queryBuilder
111
                    ->andWhere(sprintf('%s %s NULL', $alias, $value ? 'IS NOT' : 'IS'));
112
113
                return;
114
            }
115
116
            $queryBuilder
117
                ->andWhere(sprintf('%s.%s %s NULL', $alias, $field, $value ? 'IS NOT' : 'IS'));
118
119
            return;
120
        }
121
122
        if ($metadata->hasField($field)) {
123
            $queryBuilder
124
                ->andWhere(sprintf('%s.%s %s NULL', $alias, $field, $value ? 'IS NOT' : 'IS'));
125
        }
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    protected function isNullableField(string $property, string $resourceClass): bool
132
    {
133
        $propertyParts = $this->splitPropertyParts($property, $resourceClass);
134
        $metadata = $this->getNestedMetadata($resourceClass, $propertyParts['associations']);
135
136
        $field = $propertyParts['field'];
137
138
        if ($metadata->hasAssociation($field)) {
139
            if ($metadata->isSingleValuedAssociation($field)) {
140
                if (!($metadata instanceof ClassMetadataInfo)) {
141
                    return false;
142
                }
143
144
                $associationMapping = $metadata->getAssociationMapping($field);
145
146
                return $this->isAssociationNullable($associationMapping);
147
            }
148
149
            return true;
150
        }
151
152
        if ($metadata instanceof ClassMetadataInfo && $metadata->hasField($field)) {
153
            return $metadata->isNullable($field);
154
        }
155
156
        return false;
157
    }
158
159
    /**
160
     * Determines whether an association is nullable.
161
     *
162
     * @see https://github.com/doctrine/doctrine2/blob/v2.5.4/lib/Doctrine/ORM/Tools/EntityGenerator.php#L1221-L1246
163
     */
164
    private function isAssociationNullable(array $associationMapping): bool
165
    {
166
        if (!empty($associationMapping['id'])) {
167
            return false;
168
        }
169
170
        if (!isset($associationMapping['joinColumns'])) {
171
            return true;
172
        }
173
174
        $joinColumns = $associationMapping['joinColumns'];
175
        foreach ($joinColumns as $joinColumn) {
176
            if (isset($joinColumn['nullable']) && !$joinColumn['nullable']) {
177
                return false;
178
            }
179
        }
180
181
        return true;
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187
    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...
188
    {
189
        if (!$request->query->has($this->existsParameterName)) {
190
            $resourceClass = \func_num_args() > 1 ? (string) func_get_arg(1) : null;
191
192
            return parent::extractProperties($request, $resourceClass);
193
        }
194
195
        @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);
196
197
        $properties = $request->query->get($this->existsParameterName);
198
199
        return \is_array($properties) ? $properties : [];
200
    }
201
}
202