Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(elasticsearch): filter autowiring needs typings #4718

Merged
merged 1 commit into from
Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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