-
-
Notifications
You must be signed in to change notification settings - Fork 895
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(elasticsearch): filter autowiring needs typings
- Loading branch information
Showing
12 changed files
with
458 additions
and
51 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,10 +13,133 @@ | |
|
||
namespace ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter; | ||
|
||
class_exists(\ApiPlatform\Elasticsearch\Filter\AbstractFilter::class); | ||
use ApiPlatform\Core\Api\ResourceClassResolverInterface; | ||
use ApiPlatform\Core\Bridge\Elasticsearch\Util\FieldDatatypeTrait; | ||
use ApiPlatform\Core\Exception\PropertyNotFoundException; | ||
use ApiPlatform\Core\Exception\ResourceClassNotFoundException; | ||
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; | ||
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; | ||
use Symfony\Component\PropertyInfo\Type; | ||
use Symfony\Component\Serializer\NameConverter\NameConverterInterface; | ||
|
||
if (false) { | ||
class AbstractFilter extends \ApiPlatform\Elasticsearch\Filter\AbstractFilter | ||
/** | ||
* Abstract class with helpers for easing the implementation of a filter. | ||
* | ||
* @experimental | ||
* | ||
* @author Baptiste Meyer <[email protected]> | ||
*/ | ||
abstract class AbstractFilter implements FilterInterface | ||
{ | ||
use FieldDatatypeTrait { getNestedFieldPath as protected; } | ||
|
||
protected $properties; | ||
protected $propertyNameCollectionFactory; | ||
protected $nameConverter; | ||
|
||
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, ?NameConverterInterface $nameConverter = null, ?array $properties = null) | ||
{ | ||
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory; | ||
$this->propertyMetadataFactory = $propertyMetadataFactory; | ||
$this->resourceClassResolver = $resourceClassResolver; | ||
$this->nameConverter = $nameConverter; | ||
$this->properties = $properties; | ||
} | ||
|
||
/** | ||
* Gets all enabled properties for the given resource class. | ||
*/ | ||
protected function getProperties(string $resourceClass): \Traversable | ||
{ | ||
if (null !== $this->properties) { | ||
return yield from array_keys($this->properties); | ||
} | ||
|
||
try { | ||
yield from $this->propertyNameCollectionFactory->create($resourceClass); | ||
} catch (ResourceClassNotFoundException $e) { | ||
} | ||
} | ||
|
||
/** | ||
* Is the given property enabled? | ||
*/ | ||
protected function hasProperty(string $resourceClass, string $property): bool | ||
{ | ||
return \in_array($property, iterator_to_array($this->getProperties($resourceClass)), true); | ||
} | ||
|
||
/** | ||
* Gets info about the decomposed given property for the given resource class. | ||
* | ||
* Returns an array with the following info as values: | ||
* - the {@see Type} of the decomposed given property | ||
* - is the decomposed given property an association? | ||
* - the resource class of the decomposed given property | ||
* - the property name of the decomposed given property | ||
*/ | ||
protected function getMetadata(string $resourceClass, string $property): array | ||
{ | ||
$noop = [null, null, null, null]; | ||
|
||
if (!$this->hasProperty($resourceClass, $property)) { | ||
return $noop; | ||
} | ||
|
||
$properties = explode('.', $property); | ||
$totalProperties = \count($properties); | ||
$currentResourceClass = $resourceClass; | ||
$hasAssociation = false; | ||
$currentProperty = null; | ||
$type = null; | ||
|
||
foreach ($properties as $index => $currentProperty) { | ||
try { | ||
$propertyMetadata = $this->propertyMetadataFactory->create($currentResourceClass, $currentProperty); | ||
} catch (PropertyNotFoundException $e) { | ||
return $noop; | ||
} | ||
|
||
if (null === $type = $propertyMetadata->getType()) { | ||
return $noop; | ||
} | ||
|
||
++$index; | ||
$builtinType = $type->getBuiltinType(); | ||
|
||
if (Type::BUILTIN_TYPE_OBJECT !== $builtinType && Type::BUILTIN_TYPE_ARRAY !== $builtinType) { | ||
if ($totalProperties === $index) { | ||
break; | ||
} | ||
|
||
return $noop; | ||
} | ||
|
||
if ($type->isCollection() && null === $type = method_exists(Type::class, 'getCollectionValueTypes') ? ($type->getCollectionValueTypes()[0] ?? null) : $type->getCollectionValueType()) { | ||
return $noop; | ||
} | ||
|
||
if (Type::BUILTIN_TYPE_ARRAY === $builtinType && Type::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType()) { | ||
if ($totalProperties === $index) { | ||
break; | ||
} | ||
|
||
return $noop; | ||
} | ||
|
||
if (null === $className = $type->getClassName()) { | ||
return $noop; | ||
} | ||
|
||
if ($isResourceClass = $this->resourceClassResolver->isResourceClass($className)) { | ||
$currentResourceClass = $className; | ||
} elseif ($totalProperties !== $index) { | ||
return $noop; | ||
} | ||
|
||
$hasAssociation = $totalProperties === $index && $isResourceClass; | ||
} | ||
|
||
return [$type, $hasAssociation, $currentResourceClass, $currentProperty]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,10 +13,175 @@ | |
|
||
namespace ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter; | ||
|
||
class_exists(\ApiPlatform\Elasticsearch\Filter\AbstractSearchFilter::class); | ||
use ApiPlatform\Core\Api\IriConverterInterface; | ||
use ApiPlatform\Core\Api\ResourceClassResolverInterface; | ||
use ApiPlatform\Core\Bridge\Elasticsearch\Api\IdentifierExtractorInterface; | ||
use ApiPlatform\Core\Exception\InvalidArgumentException; | ||
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; | ||
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; | ||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface; | ||
use Symfony\Component\PropertyInfo\Type; | ||
use Symfony\Component\Serializer\NameConverter\NameConverterInterface; | ||
|
||
if (false) { | ||
class AbstractSearchFilter extends \ApiPlatform\Elasticsearch\Filter\AbstractSearchFilter | ||
/** | ||
* Abstract class with helpers for easing the implementation of a search filter like a term filter or a match filter. | ||
* | ||
* @experimental | ||
* | ||
* @internal | ||
* | ||
* @author Baptiste Meyer <[email protected]> | ||
*/ | ||
abstract class AbstractSearchFilter extends AbstractFilter implements ConstantScoreFilterInterface | ||
{ | ||
protected $identifierExtractor; | ||
protected $iriConverter; | ||
protected $propertyAccessor; | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, IdentifierExtractorInterface $identifierExtractor, IriConverterInterface $iriConverter, PropertyAccessorInterface $propertyAccessor, ?NameConverterInterface $nameConverter = null, ?array $properties = null) | ||
{ | ||
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceClassResolver, $nameConverter, $properties); | ||
|
||
$this->identifierExtractor = $identifierExtractor; | ||
$this->iriConverter = $iriConverter; | ||
$this->propertyAccessor = $propertyAccessor; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function apply(array $clauseBody, string $resourceClass, ?string $operationName = null, array $context = []): array | ||
{ | ||
$searches = []; | ||
|
||
foreach ($context['filters'] ?? [] as $property => $values) { | ||
[$type, $hasAssociation, $nestedResourceClass, $nestedProperty] = $this->getMetadata($resourceClass, $property); | ||
|
||
if (!$type || !$values = (array) $values) { | ||
continue; | ||
} | ||
|
||
if ($hasAssociation || $this->isIdentifier($nestedResourceClass, $nestedProperty)) { | ||
$values = array_map([$this, 'getIdentifierValue'], $values, array_fill(0, \count($values), $nestedProperty)); | ||
} | ||
|
||
if (!$this->hasValidValues($values, $type)) { | ||
continue; | ||
} | ||
|
||
$property = null === $this->nameConverter ? $property : $this->nameConverter->normalize($property, $resourceClass, null, $context); | ||
$nestedPath = $this->getNestedFieldPath($resourceClass, $property); | ||
$nestedPath = null === $nestedPath || null === $this->nameConverter ? $nestedPath : $this->nameConverter->normalize($nestedPath, $resourceClass, null, $context); | ||
|
||
$searches[] = $this->getQuery($property, $values, $nestedPath); | ||
} | ||
|
||
if (!$searches) { | ||
return $clauseBody; | ||
} | ||
|
||
return array_merge_recursive($clauseBody, [ | ||
'bool' => [ | ||
'must' => $searches, | ||
], | ||
]); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getDescription(string $resourceClass): array | ||
{ | ||
$description = []; | ||
|
||
foreach ($this->getProperties($resourceClass) as $property) { | ||
[$type, $hasAssociation] = $this->getMetadata($resourceClass, $property); | ||
|
||
if (!$type) { | ||
continue; | ||
} | ||
|
||
foreach ([$property, "${property}[]"] as $filterParameterName) { | ||
$description[$filterParameterName] = [ | ||
'property' => $property, | ||
'type' => $hasAssociation ? 'string' : $this->getPhpType($type), | ||
'required' => false, | ||
]; | ||
} | ||
} | ||
|
||
return $description; | ||
} | ||
|
||
/** | ||
* Gets the Elasticsearch query corresponding to the current search filter. | ||
*/ | ||
abstract protected function getQuery(string $property, array $values, ?string $nestedPath): array; | ||
|
||
/** | ||
* Converts the given {@see Type} in PHP type. | ||
*/ | ||
protected function getPhpType(Type $type): string | ||
{ | ||
switch ($builtinType = $type->getBuiltinType()) { | ||
case Type::BUILTIN_TYPE_ARRAY: | ||
case Type::BUILTIN_TYPE_INT: | ||
case Type::BUILTIN_TYPE_FLOAT: | ||
case Type::BUILTIN_TYPE_BOOL: | ||
case Type::BUILTIN_TYPE_STRING: | ||
return $builtinType; | ||
case Type::BUILTIN_TYPE_OBJECT: | ||
if (null !== ($className = $type->getClassName()) && is_a($className, \DateTimeInterface::class, true)) { | ||
return \DateTimeInterface::class; | ||
} | ||
|
||
// no break | ||
default: | ||
return 'string'; | ||
} | ||
} | ||
|
||
/** | ||
* Is the given property of the given resource class an identifier? | ||
*/ | ||
protected function isIdentifier(string $resourceClass, string $property): bool | ||
{ | ||
return $property === $this->identifierExtractor->getIdentifierFromResourceClass($resourceClass); | ||
} | ||
|
||
/** | ||
* Gets the ID from an IRI or a raw ID. | ||
*/ | ||
protected function getIdentifierValue(string $iri, string $property) | ||
{ | ||
try { | ||
if ($item = $this->iriConverter->getItemFromIri($iri, ['fetch_data' => false])) { | ||
return $this->propertyAccessor->getValue($item, $property); | ||
} | ||
} catch (InvalidArgumentException $e) { | ||
} | ||
|
||
return $iri; | ||
} | ||
|
||
/** | ||
* Are the given values valid according to the given {@see Type}? | ||
*/ | ||
protected function hasValidValues(array $values, Type $type): bool | ||
{ | ||
foreach ($values as $value) { | ||
if ( | ||
null !== $value | ||
&& Type::BUILTIN_TYPE_INT === $type->getBuiltinType() | ||
&& false === filter_var($value, \FILTER_VALIDATE_INT) | ||
) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,10 +13,34 @@ | |
|
||
namespace ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter; | ||
|
||
class_exists(\ApiPlatform\Elasticsearch\Filter\MatchFilter::class); | ||
|
||
if (false) { | ||
final class MatchFilter extends \ApiPlatform\Elasticsearch\Filter\MatchFilter | ||
/** | ||
* Filter the collection by given properties using a full text query. | ||
* | ||
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html | ||
* | ||
* @experimental | ||
* | ||
* @author Baptiste Meyer <[email protected]> | ||
*/ | ||
final class MatchFilter extends AbstractSearchFilter | ||
{ | ||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function getQuery(string $property, array $values, ?string $nestedPath): array | ||
{ | ||
$matches = []; | ||
|
||
foreach ($values as $value) { | ||
$matches[] = ['match' => [$property => $value]]; | ||
} | ||
|
||
$matchQuery = isset($matches[1]) ? ['bool' => ['should' => $matches]] : $matches[0]; | ||
|
||
if (null !== $nestedPath) { | ||
$matchQuery = ['nested' => ['path' => $nestedPath, 'query' => $matchQuery]]; | ||
} | ||
|
||
return $matchQuery; | ||
} | ||
} |
Oops, something went wrong.