AbstractSearchFilter::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 8
dl 0
loc 7
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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);
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Component\Serial...rInterface::normalize() has too many arguments starting with $resourceClass. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

75
            $property = null === $this->nameConverter ? $property : $this->nameConverter->/** @scrutinizer ignore-call */ normalize($property, $resourceClass, null, $context);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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
97
    {
98
        $description = [];
99
100
        foreach ($this->getProperties($resourceClass) as $property) {
101
            [$type, $hasAssociation] = $this->getMetadata($resourceClass, $property);
0 ignored issues
show
Bug introduced by
$property of type ApiPlatform\Core\Metadat...\PropertyNameCollection is incompatible with the type string expected by parameter $property of ApiPlatform\Core\Bridge\...ctFilter::getMetadata(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

101
            [$type, $hasAssociation] = $this->getMetadata($resourceClass, /** @scrutinizer ignore-type */ $property);
Loading history...
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