Skip to content

Commit 478606e

Browse files
authored
fix(elasticsearch): filter autowiring needs typings (#4718)
1 parent a93c0f6 commit 478606e

File tree

12 files changed

+458
-51
lines changed

12 files changed

+458
-51
lines changed

src/Core/Bridge/Elasticsearch/DataProvider/Filter/AbstractFilter.php

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,133 @@
1313

1414
namespace ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter;
1515

16-
class_exists(\ApiPlatform\Elasticsearch\Filter\AbstractFilter::class);
16+
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
17+
use ApiPlatform\Core\Bridge\Elasticsearch\Util\FieldDatatypeTrait;
18+
use ApiPlatform\Core\Exception\PropertyNotFoundException;
19+
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
20+
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
21+
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
22+
use Symfony\Component\PropertyInfo\Type;
23+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
1724

18-
if (false) {
19-
class AbstractFilter extends \ApiPlatform\Elasticsearch\Filter\AbstractFilter
25+
/**
26+
* Abstract class with helpers for easing the implementation of a filter.
27+
*
28+
* @experimental
29+
*
30+
* @author Baptiste Meyer <[email protected]>
31+
*/
32+
abstract class AbstractFilter implements FilterInterface
33+
{
34+
use FieldDatatypeTrait { getNestedFieldPath as protected; }
35+
36+
protected $properties;
37+
protected $propertyNameCollectionFactory;
38+
protected $nameConverter;
39+
40+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, ?NameConverterInterface $nameConverter = null, ?array $properties = null)
41+
{
42+
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
43+
$this->propertyMetadataFactory = $propertyMetadataFactory;
44+
$this->resourceClassResolver = $resourceClassResolver;
45+
$this->nameConverter = $nameConverter;
46+
$this->properties = $properties;
47+
}
48+
49+
/**
50+
* Gets all enabled properties for the given resource class.
51+
*/
52+
protected function getProperties(string $resourceClass): \Traversable
53+
{
54+
if (null !== $this->properties) {
55+
return yield from array_keys($this->properties);
56+
}
57+
58+
try {
59+
yield from $this->propertyNameCollectionFactory->create($resourceClass);
60+
} catch (ResourceClassNotFoundException $e) {
61+
}
62+
}
63+
64+
/**
65+
* Is the given property enabled?
66+
*/
67+
protected function hasProperty(string $resourceClass, string $property): bool
68+
{
69+
return \in_array($property, iterator_to_array($this->getProperties($resourceClass)), true);
70+
}
71+
72+
/**
73+
* Gets info about the decomposed given property for the given resource class.
74+
*
75+
* Returns an array with the following info as values:
76+
* - the {@see Type} of the decomposed given property
77+
* - is the decomposed given property an association?
78+
* - the resource class of the decomposed given property
79+
* - the property name of the decomposed given property
80+
*/
81+
protected function getMetadata(string $resourceClass, string $property): array
2082
{
83+
$noop = [null, null, null, null];
84+
85+
if (!$this->hasProperty($resourceClass, $property)) {
86+
return $noop;
87+
}
88+
89+
$properties = explode('.', $property);
90+
$totalProperties = \count($properties);
91+
$currentResourceClass = $resourceClass;
92+
$hasAssociation = false;
93+
$currentProperty = null;
94+
$type = null;
95+
96+
foreach ($properties as $index => $currentProperty) {
97+
try {
98+
$propertyMetadata = $this->propertyMetadataFactory->create($currentResourceClass, $currentProperty);
99+
} catch (PropertyNotFoundException $e) {
100+
return $noop;
101+
}
102+
103+
if (null === $type = $propertyMetadata->getType()) {
104+
return $noop;
105+
}
106+
107+
++$index;
108+
$builtinType = $type->getBuiltinType();
109+
110+
if (Type::BUILTIN_TYPE_OBJECT !== $builtinType && Type::BUILTIN_TYPE_ARRAY !== $builtinType) {
111+
if ($totalProperties === $index) {
112+
break;
113+
}
114+
115+
return $noop;
116+
}
117+
118+
if ($type->isCollection() && null === $type = method_exists(Type::class, 'getCollectionValueTypes') ? ($type->getCollectionValueTypes()[0] ?? null) : $type->getCollectionValueType()) {
119+
return $noop;
120+
}
121+
122+
if (Type::BUILTIN_TYPE_ARRAY === $builtinType && Type::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType()) {
123+
if ($totalProperties === $index) {
124+
break;
125+
}
126+
127+
return $noop;
128+
}
129+
130+
if (null === $className = $type->getClassName()) {
131+
return $noop;
132+
}
133+
134+
if ($isResourceClass = $this->resourceClassResolver->isResourceClass($className)) {
135+
$currentResourceClass = $className;
136+
} elseif ($totalProperties !== $index) {
137+
return $noop;
138+
}
139+
140+
$hasAssociation = $totalProperties === $index && $isResourceClass;
141+
}
142+
143+
return [$type, $hasAssociation, $currentResourceClass, $currentProperty];
21144
}
22145
}

src/Core/Bridge/Elasticsearch/DataProvider/Filter/AbstractSearchFilter.php

Lines changed: 168 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,175 @@
1313

1414
namespace ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter;
1515

16-
class_exists(\ApiPlatform\Elasticsearch\Filter\AbstractSearchFilter::class);
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;
1725

18-
if (false) {
19-
class AbstractSearchFilter extends \ApiPlatform\Elasticsearch\Filter\AbstractSearchFilter
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);
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_recursive($clauseBody, [
87+
'bool' => [
88+
'must' => $searches,
89+
],
90+
]);
91+
}
92+
93+
/**
94+
* {@inheritdoc}
95+
*/
96+
public function getDescription(string $resourceClass): array
2097
{
98+
$description = [];
99+
100+
foreach ($this->getProperties($resourceClass) as $property) {
101+
[$type, $hasAssociation] = $this->getMetadata($resourceClass, $property);
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) {
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;
21186
}
22187
}

src/Core/Bridge/Elasticsearch/DataProvider/Filter/MatchFilter.php

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,34 @@
1313

1414
namespace ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter;
1515

16-
class_exists(\ApiPlatform\Elasticsearch\Filter\MatchFilter::class);
17-
18-
if (false) {
19-
final class MatchFilter extends \ApiPlatform\Elasticsearch\Filter\MatchFilter
16+
/**
17+
* Filter the collection by given properties using a full text query.
18+
*
19+
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html
20+
*
21+
* @experimental
22+
*
23+
* @author Baptiste Meyer <[email protected]>
24+
*/
25+
final class MatchFilter extends AbstractSearchFilter
26+
{
27+
/**
28+
* {@inheritdoc}
29+
*/
30+
protected function getQuery(string $property, array $values, ?string $nestedPath): array
2031
{
32+
$matches = [];
33+
34+
foreach ($values as $value) {
35+
$matches[] = ['match' => [$property => $value]];
36+
}
37+
38+
$matchQuery = isset($matches[1]) ? ['bool' => ['should' => $matches]] : $matches[0];
39+
40+
if (null !== $nestedPath) {
41+
$matchQuery = ['nested' => ['path' => $nestedPath, 'query' => $matchQuery]];
42+
}
43+
44+
return $matchQuery;
2145
}
2246
}

0 commit comments

Comments
 (0)