Completed
Push — master ( 906aaa...a12ac4 )
by Han Hui
26s queued 13s
created

src/Bridge/Doctrine/Orm/Filter/SearchFilter.php (14 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\Api\IdentifiersExtractorInterface;
17
use ApiPlatform\Core\Api\IriConverterInterface;
18
use ApiPlatform\Core\Bridge\Doctrine\Common\Filter\SearchFilterInterface;
19
use ApiPlatform\Core\Bridge\Doctrine\Common\Filter\SearchFilterTrait;
20
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper;
21
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
22
use ApiPlatform\Core\Exception\InvalidArgumentException;
23
use Doctrine\Common\Persistence\ManagerRegistry;
24
use Doctrine\DBAL\Types\Type as DBALType;
25
use Doctrine\ORM\Mapping\ClassMetadata;
26
use Doctrine\ORM\QueryBuilder;
27
use Psr\Log\LoggerInterface;
28
use Symfony\Component\HttpFoundation\RequestStack;
29
use Symfony\Component\PropertyAccess\PropertyAccess;
30
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
31
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
32
33
/**
34
 * Filter the collection by given properties.
35
 *
36
 * @author Kévin Dunglas <[email protected]>
37
 */
38
class SearchFilter extends AbstractContextAwareFilter implements SearchFilterInterface
39
{
40
    use SearchFilterTrait;
41
42
    public const DOCTRINE_INTEGER_TYPE = DBALType::INTEGER;
43
44
    public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $requestStack, IriConverterInterface $iriConverter, PropertyAccessorInterface $propertyAccessor = null, LoggerInterface $logger = null, array $properties = null, IdentifiersExtractorInterface $identifiersExtractor = null, NameConverterInterface $nameConverter = null)
45
    {
46
        parent::__construct($managerRegistry, $requestStack, $logger, $properties, $nameConverter);
47
48
        if (null === $identifiersExtractor) {
49
            @trigger_error('Not injecting ItemIdentifiersExtractor is deprecated since API Platform 2.5 and can lead to unexpected behaviors, it will not be possible anymore in API Platform 3.0.', E_USER_DEPRECATED);
50
        }
51
52
        $this->iriConverter = $iriConverter;
53
        $this->identifiersExtractor = $identifiersExtractor;
54
        $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
55
    }
56
57
    protected function getIriConverter(): IriConverterInterface
58
    {
59
        return $this->iriConverter;
60
    }
61
62
    protected function getPropertyAccessor(): PropertyAccessorInterface
63
    {
64
        return $this->propertyAccessor;
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70
    protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
71
    {
72
        if (
73
            null === $value ||
74
            !$this->isPropertyEnabled($property, $resourceClass) ||
75
            !$this->isPropertyMapped($property, $resourceClass, true)
76
        ) {
77
            return;
78
        }
79
80
        $alias = $queryBuilder->getRootAliases()[0];
81
        $field = $property;
82
83
        $associations = [];
84
        if ($this->isPropertyNested($property, $resourceClass)) {
85
            [$alias, $field, $associations] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass);
86
        }
87
88
        /**
89
         * @var ClassMetadata
90
         */
91
        $metadata = $this->getNestedMetadata($resourceClass, $associations);
92
93
        $values = $this->normalizeValues((array) $value, $property);
94
        if (null === $values) {
95
            return;
96
        }
97
98
        $caseSensitive = true;
99
        if ($metadata->hasField($field)) {
100
            if ('id' === $field) {
101
                $values = array_map([$this, 'getIdFromValue'], $values);
102
            }
103
104
            if (!$this->hasValidValues($values, $this->getDoctrineFieldType($property, $resourceClass))) {
105
                $this->logger->notice('Invalid filter ignored', [
106
                    'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $field)),
107
                ]);
108
109
                return;
110
            }
111
112
            $strategy = $this->properties[$property] ?? self::STRATEGY_EXACT;
113
114
            // prefixing the strategy with i makes it case insensitive
115
            if (0 === strpos($strategy, 'i')) {
116
                $strategy = substr($strategy, 1);
117
                $caseSensitive = false;
118
            }
119
120
            if (1 === \count($values)) {
121
                $this->addWhereByStrategy($strategy, $queryBuilder, $queryNameGenerator, $alias, $field, $values[0], $caseSensitive);
122
123
                return;
124
            }
125
126
            if (self::STRATEGY_EXACT !== $strategy) {
127
                $this->logger->notice('Invalid filter ignored', [
128
                    'exception' => new InvalidArgumentException(sprintf('"%s" strategy selected for "%s" property, but only "%s" strategy supports multiple values', $strategy, $property, self::STRATEGY_EXACT)),
129
                ]);
130
131
                return;
132
            }
133
134
            $wrapCase = $this->createWrapCase($caseSensitive);
135
            $valueParameter = $queryNameGenerator->generateParameterName($field);
136
137
            $queryBuilder
138
                ->andWhere(sprintf($wrapCase('%s.%s').' IN (:%s)', $alias, $field, $valueParameter))
139
                ->setParameter($valueParameter, $caseSensitive ? $values : array_map('strtolower', $values));
140
        }
141
142
        // metadata doesn't have the field, nor an association on the field
143
        if (!$metadata->hasAssociation($field)) {
144
            return;
145
        }
146
147
        $values = array_map([$this, 'getIdFromValue'], $values);
148
        $associationFieldIdentifier = 'id';
149
        $doctrineTypeField = $this->getDoctrineFieldType($property, $resourceClass);
150
151
        if (null !== $this->identifiersExtractor) {
152
            $associationResourceClass = $metadata->getAssociationTargetClass($field);
153
            $associationFieldIdentifier = $this->identifiersExtractor->getIdentifiersFromResourceClass($associationResourceClass)[0];
154
            $doctrineTypeField = $this->getDoctrineFieldType($associationFieldIdentifier, $associationResourceClass);
155
        }
156
157
        if (!$this->hasValidValues($values, $doctrineTypeField)) {
158
            $this->logger->notice('Invalid filter ignored', [
159
                'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $field)),
160
            ]);
161
162
            return;
163
        }
164
165
        $association = $field;
166
        $valueParameter = $queryNameGenerator->generateParameterName($association);
167
        if ($metadata->isCollectionValuedAssociation($association)) {
168
            $associationAlias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, $alias, $association);
169
            $associationField = $associationFieldIdentifier;
170
        } else {
171
            $associationAlias = $alias;
172
            $associationField = $field;
173
        }
174
175
        if (1 === \count($values)) {
176
            $queryBuilder
177
                ->andWhere(sprintf('%s.%s = :%s', $associationAlias, $associationField, $valueParameter))
178
                ->setParameter($valueParameter, $values[0]);
179
        } else {
180
            $queryBuilder
181
                ->andWhere(sprintf('%s.%s IN (:%s)', $associationAlias, $associationField, $valueParameter))
182
                ->setParameter($valueParameter, $values);
183
        }
184
    }
185
186
    /**
187
     * Adds where clause according to the strategy.
188
     *
189
     * @throws InvalidArgumentException If strategy does not exist
190
     */
191
    protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, $value, bool $caseSensitive)
192
    {
193
        $wrapCase = $this->createWrapCase($caseSensitive);
194
        $valueParameter = $queryNameGenerator->generateParameterName($field);
195
196
        switch ($strategy) {
197
            case null:
198
            case self::STRATEGY_EXACT:
199
                $queryBuilder
200
                    ->andWhere(sprintf($wrapCase('%s.%s').' = '.$wrapCase(':%s'), $alias, $field, $valueParameter))
201
                    ->setParameter($valueParameter, $value);
202
                break;
203
            case self::STRATEGY_PARTIAL:
204
                $queryBuilder
205
                    ->andWhere(sprintf($wrapCase('%s.%s').' LIKE '.$wrapCase('CONCAT(\'%%\', :%s, \'%%\')'), $alias, $field, $valueParameter))
206
                    ->setParameter($valueParameter, $value);
207
                break;
208
            case self::STRATEGY_START:
209
                $queryBuilder
210
                    ->andWhere(sprintf($wrapCase('%s.%s').' LIKE '.$wrapCase('CONCAT(:%s, \'%%\')'), $alias, $field, $valueParameter))
211
                    ->setParameter($valueParameter, $value);
212
                break;
213
            case self::STRATEGY_END:
214
                $queryBuilder
215
                    ->andWhere(sprintf($wrapCase('%s.%s').' LIKE '.$wrapCase('CONCAT(\'%%\', :%s)'), $alias, $field, $valueParameter))
216
                    ->setParameter($valueParameter, $value);
217
                break;
218
            case self::STRATEGY_WORD_START:
219
                $queryBuilder
220
                    ->andWhere(sprintf($wrapCase('%1$s.%2$s').' LIKE '.$wrapCase('CONCAT(:%3$s, \'%%\')').' OR '.$wrapCase('%1$s.%2$s').' LIKE '.$wrapCase('CONCAT(\'%% \', :%3$s, \'%%\')'), $alias, $field, $valueParameter))
221
                    ->setParameter($valueParameter, $value);
222
                break;
223
            default:
224
                throw new InvalidArgumentException(sprintf('strategy %s does not exist.', $strategy));
225
        }
226
    }
227
228
    /**
229
     * Creates a function that will wrap a Doctrine expression according to the
230
     * specified case sensitivity.
231
     *
232
     * For example, "o.name" will get wrapped into "LOWER(o.name)" when $caseSensitive
233
     * is false.
234
     */
235
    protected function createWrapCase(bool $caseSensitive): \Closure
236
    {
237
        return function (string $expr) use ($caseSensitive): string {
238
            if ($caseSensitive) {
239
                return $expr;
240
            }
241
242
            return sprintf('LOWER(%s)', $expr);
243
        };
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     */
249
    protected function getType(string $doctrineType): string
250
    {
251
        switch ($doctrineType) {
252
            case DBALType::TARRAY:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::TARRAY has been deprecated: Use {@see DefaultTypes::ARRAY} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

252
            case /** @scrutinizer ignore-deprecated */ DBALType::TARRAY:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
253
                return 'array';
254
            case DBALType::BIGINT:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::BIGINT has been deprecated: Use {@see DefaultTypes::BIGINT} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

254
            case /** @scrutinizer ignore-deprecated */ DBALType::BIGINT:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
255
            case DBALType::INTEGER:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::INTEGER has been deprecated: Use {@see DefaultTypes::INTEGER} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

255
            case /** @scrutinizer ignore-deprecated */ DBALType::INTEGER:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
256
            case DBALType::SMALLINT:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::SMALLINT has been deprecated: Use {@see DefaultTypes::SMALLINT} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

256
            case /** @scrutinizer ignore-deprecated */ DBALType::SMALLINT:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
257
                return 'int';
258
            case DBALType::BOOLEAN:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::BOOLEAN has been deprecated: Use {@see DefaultTypes::BOOLEAN} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

258
            case /** @scrutinizer ignore-deprecated */ DBALType::BOOLEAN:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
259
                return 'bool';
260
            case DBALType::DATE:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::DATE has been deprecated: Use {@see DefaultTypes::DATE_MUTABLE} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

260
            case /** @scrutinizer ignore-deprecated */ DBALType::DATE:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
261
            case DBALType::TIME:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::TIME has been deprecated: Use {@see DefaultTypes::TIME_MUTABLE} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

261
            case /** @scrutinizer ignore-deprecated */ DBALType::TIME:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
262
            case DBALType::DATETIME:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::DATETIME has been deprecated: Use {@see DefaultTypes::DATETIME_MUTABLE} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

262
            case /** @scrutinizer ignore-deprecated */ DBALType::DATETIME:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
263
            case DBALType::DATETIMETZ:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::DATETIMETZ has been deprecated: Use {@see DefaultTypes::DATETIMETZ_MUTABLE} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

263
            case /** @scrutinizer ignore-deprecated */ DBALType::DATETIMETZ:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
264
                return \DateTimeInterface::class;
265
            case DBALType::FLOAT:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::FLOAT has been deprecated: Use {@see DefaultTypes::FLOAT} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

265
            case /** @scrutinizer ignore-deprecated */ DBALType::FLOAT:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
266
                return 'float';
267
        }
268
269
        if (\defined(DBALType::class.'::DATE_IMMUTABLE')) {
270
            switch ($doctrineType) {
271
                case DBALType::DATE_IMMUTABLE:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::DATE_IMMUTABLE has been deprecated: Use {@see DefaultTypes::DATE_IMMUTABLE} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

271
                case /** @scrutinizer ignore-deprecated */ DBALType::DATE_IMMUTABLE:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
272
                case DBALType::TIME_IMMUTABLE:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::TIME_IMMUTABLE has been deprecated: Use {@see DefaultTypes::TIME_IMMUTABLE} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

272
                case /** @scrutinizer ignore-deprecated */ DBALType::TIME_IMMUTABLE:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
273
                case DBALType::DATETIME_IMMUTABLE:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::DATETIME_IMMUTABLE has been deprecated: Use {@see DefaultTypes::DATETIME_IMMUTABLE} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

273
                case /** @scrutinizer ignore-deprecated */ DBALType::DATETIME_IMMUTABLE:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
274
                case DBALType::DATETIMETZ_IMMUTABLE:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::DATETIMETZ_IMMUTABLE has been deprecated: Use {@see DefaultTypes::DATETIMETZ_IMMUTABLE} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

274
                case /** @scrutinizer ignore-deprecated */ DBALType::DATETIMETZ_IMMUTABLE:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
275
                    return \DateTimeInterface::class;
276
            }
277
        }
278
279
        return 'string';
280
    }
281
}
282