Passed
Push — master ( 8bd912...d93388 )
by Alan
06:58 queued 02:20
created

DataProvider/Filter/AbstractSearchFilter.php (1 issue)

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);
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);
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
Consider adding a comment why this CATCH block is empty.
Loading history...
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