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\Elasticsearch\DataProvider\Filter; |
||||
15 | |||||
16 | use ApiPlatform\Core\Api\IriConverterInterface; |
||||
17 | use ApiPlatform\Core\Api\ResourceClassResolverInterface; |
||||
18 | use ApiPlatform\Core\Bridge\Elasticsearch\Api\IdentifierExtractorInterface; |
||||
19 | use ApiPlatform\Core\Exception\InvalidArgumentException; |
||||
20 | use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; |
||||
21 | use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; |
||||
22 | use Symfony\Component\PropertyAccess\PropertyAccessorInterface; |
||||
23 | use Symfony\Component\PropertyInfo\Type; |
||||
24 | use Symfony\Component\Serializer\NameConverter\NameConverterInterface; |
||||
25 | |||||
26 | /** |
||||
27 | * Abstract class with helpers for easing the implementation of a search filter like a term filter or a match filter. |
||||
28 | * |
||||
29 | * @experimental |
||||
30 | * |
||||
31 | * @internal |
||||
32 | * |
||||
33 | * @author Baptiste Meyer <[email protected]> |
||||
34 | */ |
||||
35 | abstract class AbstractSearchFilter extends AbstractFilter implements ConstantScoreFilterInterface |
||||
36 | { |
||||
37 | protected $identifierExtractor; |
||||
38 | protected $iriConverter; |
||||
39 | protected $propertyAccessor; |
||||
40 | |||||
41 | /** |
||||
42 | * {@inheritdoc} |
||||
43 | */ |
||||
44 | public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, IdentifierExtractorInterface $identifierExtractor, IriConverterInterface $iriConverter, PropertyAccessorInterface $propertyAccessor, ?NameConverterInterface $nameConverter = null, ?array $properties = null) |
||||
45 | { |
||||
46 | parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceClassResolver, $nameConverter, $properties); |
||||
47 | |||||
48 | $this->identifierExtractor = $identifierExtractor; |
||||
49 | $this->iriConverter = $iriConverter; |
||||
50 | $this->propertyAccessor = $propertyAccessor; |
||||
51 | } |
||||
52 | |||||
53 | /** |
||||
54 | * {@inheritdoc} |
||||
55 | */ |
||||
56 | public function apply(array $clauseBody, string $resourceClass, ?string $operationName = null, array $context = []): array |
||||
57 | { |
||||
58 | $searches = []; |
||||
59 | |||||
60 | foreach ($context['filters'] ?? [] as $property => $values) { |
||||
61 | [$type, $hasAssociation, $nestedResourceClass, $nestedProperty] = $this->getMetadata($resourceClass, $property); |
||||
62 | |||||
63 | if (!$type || !$values = (array) $values) { |
||||
64 | continue; |
||||
65 | } |
||||
66 | |||||
67 | if ($hasAssociation || $this->isIdentifier($nestedResourceClass, $nestedProperty)) { |
||||
68 | $values = array_map([$this, 'getIdentifierValue'], $values, array_fill(0, \count($values), $nestedProperty)); |
||||
69 | } |
||||
70 | |||||
71 | if (!$this->hasValidValues($values, $type)) { |
||||
72 | continue; |
||||
73 | } |
||||
74 | |||||
75 | $property = null === $this->nameConverter ? $property : $this->nameConverter->normalize($property, $resourceClass, null, $context); |
||||
0 ignored issues
–
show
|
|||||
76 | $nestedPath = $this->getNestedFieldPath($resourceClass, $property); |
||||
77 | $nestedPath = null === $nestedPath || null === $this->nameConverter ? $nestedPath : $this->nameConverter->normalize($nestedPath, $resourceClass, null, $context); |
||||
78 | |||||
79 | $searches[] = $this->getQuery($property, $values, $nestedPath); |
||||
80 | } |
||||
81 | |||||
82 | if (!$searches) { |
||||
83 | return $clauseBody; |
||||
84 | } |
||||
85 | |||||
86 | return array_merge($clauseBody, [ |
||||
87 | 'bool' => [ |
||||
88 | 'must' => $searches, |
||||
89 | ], |
||||
90 | ]); |
||||
91 | } |
||||
92 | |||||
93 | /** |
||||
94 | * {@inheritdoc} |
||||
95 | */ |
||||
96 | public function getDescription(string $resourceClass): array |
||||
97 | { |
||||
98 | $description = []; |
||||
99 | |||||
100 | foreach ($this->getProperties($resourceClass) as $property) { |
||||
101 | [$type, $hasAssociation] = $this->getMetadata($resourceClass, $property); |
||||
0 ignored issues
–
show
$property of type ApiPlatform\Core\Metadat...\PropertyNameCollection is incompatible with the type string expected by parameter $property of ApiPlatform\Core\Bridge\...ctFilter::getMetadata() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
102 | |||||
103 | if (!$type) { |
||||
104 | continue; |
||||
105 | } |
||||
106 | |||||
107 | foreach ([$property, "${property}[]"] as $filterParameterName) { |
||||
108 | $description[$filterParameterName] = [ |
||||
109 | 'property' => $property, |
||||
110 | 'type' => $hasAssociation ? 'string' : $this->getPhpType($type), |
||||
111 | 'required' => false, |
||||
112 | ]; |
||||
113 | } |
||||
114 | } |
||||
115 | |||||
116 | return $description; |
||||
117 | } |
||||
118 | |||||
119 | /** |
||||
120 | * Gets the Elasticsearch query corresponding to the current search filter. |
||||
121 | */ |
||||
122 | abstract protected function getQuery(string $property, array $values, ?string $nestedPath): array; |
||||
123 | |||||
124 | /** |
||||
125 | * Converts the given {@see Type} in PHP type. |
||||
126 | */ |
||||
127 | protected function getPhpType(Type $type): string |
||||
128 | { |
||||
129 | switch ($builtinType = $type->getBuiltinType()) { |
||||
130 | case Type::BUILTIN_TYPE_ARRAY: |
||||
131 | case Type::BUILTIN_TYPE_INT: |
||||
132 | case Type::BUILTIN_TYPE_FLOAT: |
||||
133 | case Type::BUILTIN_TYPE_BOOL: |
||||
134 | case Type::BUILTIN_TYPE_STRING: |
||||
135 | return $builtinType; |
||||
136 | case Type::BUILTIN_TYPE_OBJECT: |
||||
137 | if (null !== ($className = $type->getClassName()) && is_a($className, \DateTimeInterface::class, true)) { |
||||
138 | return \DateTimeInterface::class; |
||||
139 | } |
||||
140 | |||||
141 | // no break |
||||
142 | default: |
||||
143 | return 'string'; |
||||
144 | } |
||||
145 | } |
||||
146 | |||||
147 | /** |
||||
148 | * Is the given property of the given resource class an identifier? |
||||
149 | */ |
||||
150 | protected function isIdentifier(string $resourceClass, string $property): bool |
||||
151 | { |
||||
152 | return $property === $this->identifierExtractor->getIdentifierFromResourceClass($resourceClass); |
||||
153 | } |
||||
154 | |||||
155 | /** |
||||
156 | * Gets the ID from an IRI or a raw ID. |
||||
157 | */ |
||||
158 | protected function getIdentifierValue(string $iri, string $property) |
||||
159 | { |
||||
160 | try { |
||||
161 | if ($item = $this->iriConverter->getItemFromIri($iri, ['fetch_data' => false])) { |
||||
162 | return $this->propertyAccessor->getValue($item, $property); |
||||
163 | } |
||||
164 | } catch (InvalidArgumentException $e) { |
||||
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
|
|||||
165 | } |
||||
166 | |||||
167 | return $iri; |
||||
168 | } |
||||
169 | |||||
170 | /** |
||||
171 | * Are the given values valid according to the given {@see Type}? |
||||
172 | */ |
||||
173 | protected function hasValidValues(array $values, Type $type): bool |
||||
174 | { |
||||
175 | foreach ($values as $value) { |
||||
176 | if ( |
||||
177 | null !== $value |
||||
178 | && Type::BUILTIN_TYPE_INT === $type->getBuiltinType() |
||||
179 | && false === filter_var($value, FILTER_VALIDATE_INT) |
||||
180 | ) { |
||||
181 | return false; |
||||
182 | } |
||||
183 | } |
||||
184 | |||||
185 | return true; |
||||
186 | } |
||||
187 | } |
||||
188 |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.