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
|
|||||
253 | return 'array'; |
||||
254 | case DBALType::BIGINT: |
||||
0 ignored issues
–
show
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 |
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.