Passed
Push — master ( 8bd912...d93388 )
by Alan
06:58 queued 02:20
created

src/Bridge/Doctrine/Orm/Filter/ExistsFilter.php (2 issues)

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
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
28
29
/**
30
 * Filters the collection by whether a property value exists or not.
31
 *
32
 * For each property passed, if the resource does not have such property or if
33
 * the value is not one of ( "true" | "false" | "1" | "0" ) the property is ignored.
34
 *
35
 * A query parameter with key but no value is treated as `true`, e.g.:
36
 * Request: GET /products?exists[brand]
37
 * Interpretation: filter products which have a brand
38
 *
39
 * @author Teoh Han Hui <[email protected]>
40
 */
41
class ExistsFilter extends AbstractContextAwareFilter implements ExistsFilterInterface
42
{
43
    use ExistsFilterTrait;
44
45
    public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $requestStack = null, LoggerInterface $logger = null, array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, NameConverterInterface $nameConverter = null)
46
    {
47
        parent::__construct($managerRegistry, $requestStack, $logger, $properties, $nameConverter);
48
49
        $this->existsParameterName = $existsParameterName;
50
    }
51
52
    /**
53
     * {@inheritdoc}
54
     */
55
    public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = [])
56
    {
57
        if (!\is_array($context['filters'][$this->existsParameterName] ?? null)) {
58
            $context['exists_deprecated_syntax'] = true;
59
            parent::apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
60
61
            return;
62
        }
63
64
        foreach ($context['filters'][$this->existsParameterName] as $property => $value) {
65
            $this->filterProperty($this->denormalizePropertyName($property), $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
66
        }
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72
    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...
73
    {
74
        if (\func_num_args() > 6) {
75
            $context = func_get_arg(6);
76
        } else {
77
            if (__CLASS__ !== \get_class($this)) {
78
                $r = new \ReflectionMethod($this, __FUNCTION__);
79
                if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
80
                    @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);
81
                }
82
            }
83
            $context = [];
84
        }
85
86
        if (
87
            (($context['exists_deprecated_syntax'] ?? false) && !isset($value[self::QUERY_PARAMETER_KEY])) ||
88
            !$this->isPropertyEnabled($property, $resourceClass) ||
89
            !$this->isPropertyMapped($property, $resourceClass, true) ||
90
            !$this->isNullableField($property, $resourceClass)
91
        ) {
92
            return;
93
        }
94
95
        $value = $this->normalizeValue($value, $property);
96
        if (null === $value) {
97
            return;
98
        }
99
100
        $alias = $queryBuilder->getRootAliases()[0];
101
        $field = $property;
102
103
        $associations = [];
104
        if ($this->isPropertyNested($property, $resourceClass)) {
105
            [$alias, $field, $associations] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass);
106
        }
107
        $metadata = $this->getNestedMetadata($resourceClass, $associations);
108
109
        if ($metadata->hasAssociation($field)) {
110
            if ($metadata->isCollectionValuedAssociation($field)) {
111
                $queryBuilder
112
                    ->andWhere(sprintf('%s.%s %s EMPTY', $alias, $field, $value ? 'IS NOT' : 'IS'));
113
114
                return;
115
            }
116
117
            if ($metadata->isAssociationInverseSide($field)) {
118
                $alias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, $alias, $field, Join::LEFT_JOIN);
119
120
                $queryBuilder
121
                    ->andWhere(sprintf('%s %s NULL', $alias, $value ? 'IS NOT' : 'IS'));
122
123
                return;
124
            }
125
126
            $queryBuilder
127
                ->andWhere(sprintf('%s.%s %s NULL', $alias, $field, $value ? 'IS NOT' : 'IS'));
128
129
            return;
130
        }
131
132
        if ($metadata->hasField($field)) {
133
            $queryBuilder
134
                ->andWhere(sprintf('%s.%s %s NULL', $alias, $field, $value ? 'IS NOT' : 'IS'));
135
        }
136
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141
    protected function isNullableField(string $property, string $resourceClass): bool
142
    {
143
        $propertyParts = $this->splitPropertyParts($property, $resourceClass);
144
        $metadata = $this->getNestedMetadata($resourceClass, $propertyParts['associations']);
145
146
        $field = $propertyParts['field'];
147
148
        if ($metadata->hasAssociation($field)) {
149
            if ($metadata->isSingleValuedAssociation($field)) {
150
                if (!($metadata instanceof ClassMetadataInfo)) {
151
                    return false;
152
                }
153
154
                $associationMapping = $metadata->getAssociationMapping($field);
155
156
                return $this->isAssociationNullable($associationMapping);
157
            }
158
159
            return true;
160
        }
161
162
        if ($metadata instanceof ClassMetadataInfo && $metadata->hasField($field)) {
163
            return $metadata->isNullable($field);
164
        }
165
166
        return false;
167
    }
168
169
    /**
170
     * Determines whether an association is nullable.
171
     *
172
     * @see https://github.com/doctrine/doctrine2/blob/v2.5.4/lib/Doctrine/ORM/Tools/EntityGenerator.php#L1221-L1246
173
     */
174
    private function isAssociationNullable(array $associationMapping): bool
175
    {
176
        if (!empty($associationMapping['id'])) {
177
            return false;
178
        }
179
180
        if (!isset($associationMapping['joinColumns'])) {
181
            return true;
182
        }
183
184
        $joinColumns = $associationMapping['joinColumns'];
185
        foreach ($joinColumns as $joinColumn) {
186
            if (isset($joinColumn['nullable']) && !$joinColumn['nullable']) {
187
                return false;
188
            }
189
        }
190
191
        return true;
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197
    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...
198
    {
199
        if (!$request->query->has($this->existsParameterName)) {
200
            $resourceClass = \func_num_args() > 1 ? (string) func_get_arg(1) : null;
201
202
            return parent::extractProperties($request, $resourceClass);
203
        }
204
205
        @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);
206
207
        $properties = $request->query->get($this->existsParameterName);
208
209
        return \is_array($properties) ? $properties : [];
210
    }
211
}
212