Skip to content

Commit

Permalink
fix(elasticsearch): filter autowiring needs typings
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Apr 20, 2022
1 parent ee0fabb commit 400d499
Show file tree
Hide file tree
Showing 12 changed files with 458 additions and 51 deletions.
129 changes: 126 additions & 3 deletions src/Core/Bridge/Elasticsearch/DataProvider/Filter/AbstractFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
32 changes: 28 additions & 4 deletions src/Core/Bridge/Elasticsearch/DataProvider/Filter/MatchFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Loading

0 comments on commit 400d499

Please sign in to comment.