OptionsMethodRequest   B
last analyzed

Complexity

Total Complexity 46

Size/Duplication

Total Lines 300
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 97
dl 0
loc 300
rs 8.72
c 3
b 0
f 0
wmc 46

15 Methods

Rating   Name   Duplication   Size   Complexity  
A expandInputParameter() 0 22 4
A __invoke() 0 3 1
A setParameterType() 0 9 2
A getRequiredWithoutExpandedParams() 0 17 4
A setParamMetas() 0 12 3
A getType() 0 10 2
A getConstructorParamType() 0 10 3
A processRegularParameter() 0 14 3
A buildConstructorParamDoc() 0 15 3
A processInputParameter() 0 22 3
A getConstructor() 0 10 3
A setParameterDefault() 0 8 4
A getParameterType() 0 9 3
A getParamMetas() 0 24 4
A getDefaultValueString() 0 9 4

How to fix   Complexity   

Complex Class

Complex classes like OptionsMethodRequest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OptionsMethodRequest, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace BEAR\Resource;
6
7
use Ray\InputQuery\Attribute\Input;
8
use ReflectionAttribute;
9
use ReflectionClass;
10
use ReflectionMethod;
11
use ReflectionNamedType;
12
use ReflectionParameter;
13
14
use function array_unique;
15
use function array_values;
16
use function assert;
17
use function in_array;
18
use function is_array;
19
use function is_string;
20
use function method_exists;
21
22
/**
23
 * @psalm-import-type OptionsResponse from Types
24
 * @psalm-import-type InsMap from Types
25
 * @psalm-import-type ParameterMetadata from Types
26
 * @psalm-import-type ParametersMap from Types
27
 * @psalm-import-type RequiredParameterList from Types
28
 * @psalm-import-type ReflectionParameterList from Types
29
 */
30
final class OptionsMethodRequest
31
{
32
    /**
33
     * @param ParametersMap $paramDoc
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\ParametersMap was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
34
     * @param InsMap        $ins
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\InsMap was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
35
     *
36
     * @return OptionsResponse
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\OptionsResponse was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
37
     */
38
    public function __invoke(ReflectionMethod $method, array $paramDoc, array $ins): array
39
    {
40
        return $this->getParamMetas($method->getParameters(), $paramDoc, $ins);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getParamMe...ers(), $paramDoc, $ins) returns the type array which is incompatible with the documented return type BEAR\Resource\OptionsResponse.
Loading history...
41
    }
42
43
    /**
44
     * @param ParametersMap $paramDoc
45
     *
46
     * @psalm-suppress RedundantCondition for BC
47
     */
48
    private function getParameterType(ReflectionParameter $parameter, array $paramDoc, string $name): string|null
49
    {
50
        /** @phpstan-ignore function.alreadyNarrowedType */
51
        $hasType = method_exists($parameter, 'getType') && $parameter->getType();
52
        if ($hasType) {
53
            return $this->getType($parameter);
54
        }
55
56
        return $paramDoc[$name]['type'] ?? null;
57
    }
58
59
    /**
60
     * @param ReflectionParameterList $parameters
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\ReflectionParameterList was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
61
     * @param ParametersMap           $paramDoc
62
     * @param InsMap                  $ins
63
     *
64
     * @return OptionsResponse
65
     */
66
    private function getParamMetas(array $parameters, array $paramDoc, array $ins): array
67
    {
68
        $expandedParameters = [];
69
        $expandedRequired = [];
70
        $expandedParamNames = [];
71
72
        foreach ($parameters as $parameter) {
73
            if ($this->processInputParameter($parameter, $expandedParameters, $expandedRequired, $expandedParamNames)) {
74
                continue;
75
            }
76
77
            $this->processRegularParameter($parameter, $paramDoc, $ins);
78
        }
79
80
        // Merge expanded parameters with regular parameters
81
        $paramDoc = $expandedParameters + $paramDoc;
82
83
        $required = $this->getRequiredWithoutExpandedParams($parameters, $expandedParamNames);
84
        // Merge expanded required with regular required
85
        if ($expandedRequired !== []) {
86
            $required = array_values(array_unique([...$expandedRequired, ...$required]));
87
        }
88
89
        return $this->setParamMetas($paramDoc, $required);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->setParamMetas($paramDoc, $required) returns the type array which is incompatible with the documented return type BEAR\Resource\OptionsResponse.
Loading history...
90
    }
91
92
    /**
93
     * Process #[Input] parameter and accumulate expanded data
94
     *
95
     * @param ParametersMap $expandedParameters
96
     * @param list<string>  $expandedRequired
97
     * @param list<string>  $expandedParamNames
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
98
     *
99
     * @return bool True if parameter was processed as Input parameter
100
     */
101
    private function processInputParameter(
102
        ReflectionParameter $parameter,
103
        array &$expandedParameters,
104
        array &$expandedRequired,
105
        array &$expandedParamNames,
106
    ): bool {
107
        $inputAttributes = $parameter->getAttributes(Input::class, ReflectionAttribute::IS_INSTANCEOF);
108
        if (! $inputAttributes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inputAttributes of type ReflectionAttribute[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
109
            return false;
110
        }
111
112
        $inputResult = $this->expandInputParameter($parameter);
113
        if ($inputResult === null) {
114
            return false;
115
        }
116
117
        [$inputParamDoc, $inputRequired] = $inputResult;
118
        $expandedParameters += $inputParamDoc;
119
        $expandedRequired = [...$expandedRequired, ...$inputRequired];
120
        $expandedParamNames[] = $parameter->name;
121
122
        return true;
123
    }
124
125
    /**
126
     * Process regular (non-Input) parameter
127
     *
128
     * @param ParametersMap $paramDoc
129
     * @param InsMap        $ins
130
     */
131
    private function processRegularParameter(ReflectionParameter $parameter, array &$paramDoc, array $ins): void
132
    {
133
        $name = $parameter->name;
134
135
        if (isset($ins[$name])) {
136
            $paramDoc[$name]['in'] = $ins[$name];
137
        }
138
139
        if (! isset($paramDoc[$name])) {
140
            $paramDoc[$name] = [];
141
        }
142
143
        $this->setParameterType($paramDoc, $parameter);
144
        $this->setParameterDefault($paramDoc, $parameter);
145
    }
146
147
    /**
148
     * Expand #[Input] parameter to its constructor properties
149
     *
150
     * @return array{0: ParametersMap, 1: RequiredParameterList}|null
151
     */
152
    private function expandInputParameter(ReflectionParameter $parameter): array|null
153
    {
154
        $constructor = $this->getConstructor($parameter);
155
        if ($constructor === null) {
156
            return null;
157
        }
158
159
        $paramDoc = [];
160
        $required = [];
161
162
        foreach ($constructor->getParameters() as $ctorParam) {
163
            $name = $ctorParam->getName();
164
            $paramDoc[$name] = $this->buildConstructorParamDoc($ctorParam);
165
166
            if ($ctorParam->isOptional()) {
167
                continue;
168
            }
169
170
            $required[] = $name;
171
        }
172
173
        return [$paramDoc, $required];
174
    }
175
176
    /**
177
     * Get constructor from parameter type if it's a class with constructor
178
     */
179
    private function getConstructor(ReflectionParameter $parameter): ReflectionMethod|null
180
    {
181
        $type = $parameter->getType();
182
        if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) {
183
            return null;
184
        }
185
186
        $refClass = new ReflectionClass($type->getName());
187
188
        return $refClass->getConstructor();
189
    }
190
191
    /**
192
     * Build parameter documentation for a constructor parameter
193
     *
194
     * @return array<string, string>
195
     */
196
    private function buildConstructorParamDoc(ReflectionParameter $ctorParam): array
197
    {
198
        $doc = [];
199
200
        $typeName = $this->getConstructorParamType($ctorParam);
201
        if ($typeName !== null) {
202
            $doc['type'] = $typeName;
203
        }
204
205
        $default = $this->getDefaultValueString($ctorParam);
206
        if ($default !== null) {
207
            $doc['default'] = $default;
208
        }
209
210
        return $doc;
211
    }
212
213
    /**
214
     * Get type name from constructor parameter
215
     */
216
    private function getConstructorParamType(ReflectionParameter $ctorParam): string|null
217
    {
218
        $ctorType = $ctorParam->getType();
219
        if (! $ctorType instanceof ReflectionNamedType) {
220
            return null;
221
        }
222
223
        $typeName = $ctorType->getName();
224
225
        return $typeName === 'int' ? 'integer' : $typeName;
226
    }
227
228
    /**
229
     * Get default value as string representation
230
     */
231
    private function getDefaultValueString(ReflectionParameter $param): string|null
232
    {
233
        if (! $param->isDefaultValueAvailable() || $param->getDefaultValue() === null) {
234
            return null;
235
        }
236
237
        $default = $param->getDefaultValue();
238
239
        return is_array($default) ? '[]' : (string) $default;
240
    }
241
242
    /**
243
     * Get required parameters excluding successfully expanded #[Input] parameters
244
     *
245
     * @param ReflectionParameterList $parameters
246
     * @param list<string>            $expandedParamNames Names of parameters that were expanded
247
     *
248
     * @return RequiredParameterList
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\RequiredParameterList was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
249
     */
250
    private function getRequiredWithoutExpandedParams(array $parameters, array $expandedParamNames): array
251
    {
252
        $required = [];
253
        foreach ($parameters as $parameter) {
254
            if ($parameter->isOptional()) {
255
                continue;
256
            }
257
258
            // Skip parameters that were successfully expanded
259
            if (in_array($parameter->name, $expandedParamNames, true)) {
260
                continue;
261
            }
262
263
            $required[] = $parameter->name;
264
        }
265
266
        return $required;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $required returns the type array which is incompatible with the documented return type BEAR\Resource\RequiredParameterList.
Loading history...
267
    }
268
269
    /**
270
     * Set parameter default value if available
271
     *
272
     * @param ParametersMap $paramDoc
273
     */
274
    private function setParameterDefault(array &$paramDoc, ReflectionParameter $parameter): void
275
    {
276
        if (! $parameter->isDefaultValueAvailable() || $parameter->getDefaultValue() === null) {
277
            return;
278
        }
279
280
        $default = $parameter->getDefaultValue();
281
        $paramDoc[$parameter->name]['default'] = is_array($default) ? '[]' : (string) $default;
282
    }
283
284
    /**
285
     * Set parameter type from reflection
286
     *
287
     * @param ParametersMap $paramDoc
288
     */
289
    private function setParameterType(array &$paramDoc, ReflectionParameter $parameter): void
290
    {
291
        $type = $this->getParameterType($parameter, $paramDoc, $parameter->name);
292
        if (! is_string($type)) {
293
            return;
294
        }
295
296
        // Override type parameter by reflection over phpdoc param type
297
        $paramDoc[$parameter->name]['type'] = $type;
298
    }
299
300
    private function getType(ReflectionParameter $parameter): string
301
    {
302
        $namedType = $parameter->getType();
303
        assert($namedType instanceof ReflectionNamedType);
304
        $type = $namedType->getName();
305
        if ($type === 'int') {
306
            $type = 'integer';
307
        }
308
309
        return $type;
310
    }
311
312
    /**
313
     * @param ParametersMap         $paramDoc
314
     * @param RequiredParameterList $required
315
     *
316
     * @return OptionsResponse
317
     */
318
    private function setParamMetas(array $paramDoc, array $required): array
319
    {
320
        $paramMetas = [];
321
        if ($paramDoc !== []) {
322
            $paramMetas['parameters'] = $paramDoc;
323
        }
324
325
        if ($required !== []) {
326
            $paramMetas['required'] = $required;
327
        }
328
329
        return $paramMetas;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $paramMetas returns the type array which is incompatible with the documented return type BEAR\Resource\OptionsResponse.
Loading history...
330
    }
331
}
332