Passed
Pull Request — 1.x (#342)
by Akihito
01:33
created

OptionsMethodRequest::buildConstructorParamDoc()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 4
nop 1
dl 0
loc 15
ccs 0
cts 0
cp 0
crap 12
rs 10
c 0
b 0
f 0
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 ReflectionException;
11
use ReflectionMethod;
12
use ReflectionNamedType;
13
use ReflectionParameter;
14
15 7
use function array_unique;
16
use function array_values;
17 7
use function assert;
18 7
use function in_array;
19
use function is_array;
20 7
use function is_string;
21
use function method_exists;
22 7
23
/**
24 7
 * @psalm-import-type OptionsResponse from Types
25
 * @psalm-import-type InsMap from Types
26
 * @psalm-import-type ParameterMetadata from Types
27
 * @psalm-import-type ParametersMap from Types
28
 * @psalm-import-type RequiredParameterList from Types
29
 * @psalm-import-type ReflectionParameterList from Types
30 7
 */
31
final class OptionsMethodRequest
32 7
{
33 7
    /**
34 7
     * @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...
35
     * @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...
36 3
     *
37 2
     * @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...
38
     */
39 1
    public function __invoke(ReflectionMethod $method, array $paramDoc, array $ins): array
40
    {
41 7
        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...
42
    }
43 7
44 7
    /**
45 2
     * @param ParametersMap $paramDoc
46
     *
47 7
     * @psalm-suppress RedundantCondition for BC
48 4
     */
49
    private function getParameterType(ReflectionParameter $parameter, array $paramDoc, string $name): string|null
50 7
    {
51 7
        /** @phpstan-ignore function.alreadyNarrowedType */
52
        $hasType = method_exists($parameter, 'getType') && $parameter->getType();
53 7
        if ($hasType) {
54
            return $this->getType($parameter);
55 7
        }
56
57
        return $paramDoc[$name]['type'] ?? null;
58
    }
59
60
    /**
61 7
     * @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...
62
     * @param ParametersMap           $paramDoc
63 7
     * @param InsMap                  $ins
64 7
     *
65 7
     * @return OptionsResponse
66 7
     */
67
    private function getParamMetas(array $parameters, array $paramDoc, array $ins): array
68
    {
69
        $expandedParameters = [];
70 7
        $expandedRequired = [];
71
        $expandedParamNames = [];
72
73 7
        foreach ($parameters as $parameter) {
74
            // Check for #[Input] attribute with object type
75 7
            $inputAttributes = $parameter->getAttributes(Input::class, ReflectionAttribute::IS_INSTANCEOF);
76 7
            if ($inputAttributes) {
77 2
                $inputResult = $this->expandInputParameter($parameter);
78
                if ($inputResult !== null) {
79
                    [$inputParamDoc, $inputRequired] = $inputResult;
80 7
                    $expandedParameters += $inputParamDoc;
81
                    $expandedRequired = [...$expandedRequired, ...$inputRequired];
82
                    $expandedParamNames[] = $parameter->name;
83 7
                    continue;
84
                }
85 7
            }
86 7
87 7
            $name = (string) $parameter->name;
88
            if (isset($ins[$name])) {
89
                $paramDoc[$name]['in'] = $ins[$parameter->name];
90 7
            }
91
92
            if (! isset($paramDoc[$parameter->name])) {
93 7
                $paramDoc[$name] = [];
94
            }
95 7
96 7
            $paramDoc = $this->paramType($paramDoc, $parameter);
97 4
            $paramDoc = $this->paramDefault($paramDoc, $parameter);
98
        }
99
100 7
        // Merge expanded parameters with regular parameters
101
        $paramDoc = $expandedParameters + $paramDoc;
102
103
        $required = $this->getRequiredWithoutExpandedParams($parameters, $expandedParamNames);
104
        // Merge expanded required with regular required
105
        if ($expandedRequired !== []) {
106 7
            $required = array_values(array_unique([...$expandedRequired, ...$required]));
107
        }
108 7
109 7
        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...
110 5
    }
111 1
112 1
    /**
113
     * Expand #[Input] parameter to its constructor properties
114 5
     *
115 5
     * @return array{0: ParametersMap, 1: RequiredParameterList}|null
116
     */
117
    private function expandInputParameter(ReflectionParameter $parameter): array|null
118
    {
119 7
        $constructor = $this->getConstructor($parameter);
120
        if ($constructor === null) {
121
            return null;
122
        }
123
124
        $paramDoc = [];
125 1
        $required = [];
126
127 1
        foreach ($constructor->getParameters() as $ctorParam) {
128 1
            $name = $ctorParam->getName();
129 1
            $paramDoc[$name] = $this->buildConstructorParamDoc($ctorParam);
130
131
            if ($ctorParam->isOptional()) {
132 1
                continue;
133
            }
134
135 7
            $required[] = $name;
136
        }
137 7
138 7
        return [$paramDoc, $required];
139 7
    }
140
141 7
    /**
142 7
     * Get constructor from parameter type if it's a class with constructor
143
     */
144
    private function getConstructor(ReflectionParameter $parameter): ReflectionMethod|null
145 7
    {
146
        $type = $parameter->getType();
147
        if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) {
148
            return null;
149
        }
150
151
        $refClass = new ReflectionClass($type->getName());
152
153
        return $refClass->getConstructor();
154
    }
155
156
    /**
157
     * Build parameter documentation for a constructor parameter
158
     *
159
     * @return array<string, string>
160
     */
161
    private function buildConstructorParamDoc(ReflectionParameter $ctorParam): array
162
    {
163
        $doc = [];
164
165
        $typeName = $this->getConstructorParamType($ctorParam);
166
        if ($typeName !== null) {
167
            $doc['type'] = $typeName;
168
        }
169
170
        $default = $this->getDefaultValueString($ctorParam);
171
        if ($default !== null) {
172
            $doc['default'] = $default;
173
        }
174
175
        return $doc;
176
    }
177
178
    /**
179
     * Get type name from constructor parameter
180
     */
181
    private function getConstructorParamType(ReflectionParameter $ctorParam): string|null
182
    {
183
        $ctorType = $ctorParam->getType();
184
        if (! $ctorType instanceof ReflectionNamedType) {
185
            return null;
186
        }
187
188
        $typeName = $ctorType->getName();
189
190
        return $typeName === 'int' ? 'integer' : $typeName;
191
    }
192
193
    /**
194
     * Get default value as string representation
195
     */
196
    private function getDefaultValueString(ReflectionParameter $param): string|null
197
    {
198
        if (! $param->isDefaultValueAvailable() || $param->getDefaultValue() === null) {
199
            return null;
200
        }
201
202
        $default = $param->getDefaultValue();
203
204
        return is_array($default) ? '[]' : (string) $default;
205
    }
206
207
    /**
208
     * Get required parameters excluding successfully expanded #[Input] parameters
209
     *
210
     * @param ReflectionParameterList $parameters
211
     * @param list<string>            $expandedParamNames Names of parameters that were expanded
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...
212
     *
213
     * @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...
214
     */
215
    private function getRequiredWithoutExpandedParams(array $parameters, array $expandedParamNames): array
216
    {
217
        $required = [];
218
        foreach ($parameters as $parameter) {
219
            if ($parameter->isOptional()) {
220
                continue;
221
            }
222
223
            // Skip parameters that were successfully expanded
224
            if (in_array($parameter->name, $expandedParamNames, true)) {
225
                continue;
226
            }
227
228
            $required[] = $parameter->name;
229
        }
230
231
        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...
232
    }
233
234
    /**
235
     * @param ParametersMap $paramDoc
236
     *
237
     * @return ParametersMap
238
     *
239
     * @throws ReflectionException
240
     */
241
    private function paramDefault(array $paramDoc, ReflectionParameter $parameter): array
242
    {
243
        $hasDefault = $parameter->isDefaultValueAvailable() && $parameter->getDefaultValue() !== null;
244
        if ($hasDefault) {
245
            $default = $parameter->getDefaultValue();
246
            $paramDoc[(string) $parameter->name]['default'] = is_array($default) ? '[]' : (string) $parameter->getDefaultValue(); // @phpstan-ignore-lines
247
        }
248
249
        return $paramDoc;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $paramDoc returns the type array which is incompatible with the documented return type BEAR\Resource\ParametersMap.
Loading history...
250
    }
251
252
    /**
253
     * @param ParametersMap $paramDoc
254
     *
255
     * @return ParametersMap
256
     */
257
    private function paramType(array $paramDoc, ReflectionParameter $parameter): array
258
    {
259
        $type = $this->getParameterType($parameter, $paramDoc, $parameter->name);
260
        if (is_string($type)) {
261
            $paramDoc[(string) $parameter->name]['type'] = $type; // override type parameter by reflection over phpdoc param type
262
        }
263
264
        return $paramDoc;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $paramDoc returns the type array which is incompatible with the documented return type BEAR\Resource\ParametersMap.
Loading history...
265
    }
266
267
    private function getType(ReflectionParameter $parameter): string
268
    {
269
        $namedType = $parameter->getType();
270
        assert($namedType instanceof ReflectionNamedType);
271
        $type = $namedType->getName();
272
        if ($type === 'int') {
273
            $type = 'integer';
274
        }
275
276
        return $type;
277
    }
278
279
    /**
280
     * @param ParametersMap         $paramDoc
281
     * @param RequiredParameterList $required
282
     *
283
     * @return OptionsResponse
284
     */
285
    private function setParamMetas(array $paramDoc, array $required): array
286
    {
287
        $paramMetas = [];
288
        if ((bool) $paramDoc) {
289
            $paramMetas['parameters'] = $paramDoc;
290
        }
291
292
        if ((bool) $required) {
293
            $paramMetas['required'] = $required;
294
        }
295
296
        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...
297
    }
298
}
299