Passed
Push — php82 ( debfb6...c2bad3 )
by Akihito
02:27
created

NamedParamMetas::getDefault()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BEAR\Resource;
6
7
use BEAR\Resource\Annotation\RequestParamInterface;
8
use BEAR\Resource\Annotation\ResourceParam;
9
use Override;
10
use Ray\Aop\ReflectionMethod;
11
use Ray\Di\Di\Assisted;
12
use Ray\InputQuery\Attribute\Input;
13
use Ray\InputQuery\Attribute\InputFile;
14
use Ray\InputQuery\FileUploadFactoryInterface;
15
use Ray\InputQuery\InputQueryInterface;
16
use Ray\WebContextParam\Annotation\AbstractWebContextParam;
17
use ReflectionAttribute;
18
use ReflectionNamedType;
19
use ReflectionParameter;
20
21
/**
22
 * @psalm-import-type ParamMap from Types
23
 * @psalm-import-type WebContextParamMap from Types
24
 * @psalm-import-type ReflectionParameterMap from Types
25
 * @psalm-import-type ObjectList from Types
26
 */
27
final readonly class NamedParamMetas implements NamedParamMetasInterface
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_READONLY, expecting T_CLASS on line 27 at column 6
Loading history...
28
{
29
    /** @param InputQueryInterface<object> $inputQuery */
30
    public function __construct(
31
        private InputQueryInterface $inputQuery,
32
        private FileUploadFactoryInterface $factory,
33
    ) {
34
    }
35
36
    /**
37
     * {@inheritDoc}
38
     */
39
    #[Override]
40
    public function __invoke(callable $callable): array
41
    {
42
        // callable is [object, string] but native type doesn't allow array access
43
        /** @psalm-suppress InvalidArrayAccess, MixedArgument */
44
        /** @var array{0:object, 1:string} $callable */
45
        $method = new ReflectionMethod($callable[0], $callable[1]); // @phpstan-ignore-line
46
        $paramMetas = $this->getAttributeParamMetas($method);
47
48
        if (! $paramMetas) {
49
            $paramMetas = $this->getAnnotationParamMetas($method);
50
        }
51
52
        return $paramMetas;
53
    }
54
55
    /** @return array<string, AssistedWebContextParam|ParamInterface> */
56
    private function getAnnotationParamMetas(ReflectionMethod $method): array
57
    {
58
        $parameters = $method->getParameters();
59
        $annotations = $method->getAnnotations();
60
        $assistedNames = $this->getAssistedNames($annotations);
61
        $webContext = $this->getWebContext($annotations);
62
63
        return $this->addNamedParams($parameters, $assistedNames, $webContext);
64
    }
65
66
    /**
67
     * @return ParamMap
68
     *
69
     * @psalm-suppress TooManyTemplateParams $refAttribute
70
     * @psalm-suppress PossiblyInvalidArrayAssignment
71
     */
72
    private function getAttributeParamMetas(ReflectionMethod $method): array
73
    {
74
        $parameters = $method->getParameters();
75
        $names = $valueParams = [];
76
        foreach ($parameters as $parameter) {
77
            $refAttribute = $parameter->getAttributes(RequestParamInterface::class, ReflectionAttribute::IS_INSTANCEOF);
78
            if ($refAttribute) {
79
                /** @var ?ResourceParam $resourceParam */
80
                $resourceParam = $refAttribute[0]->newInstance();
81
                if ($resourceParam instanceof ResourceParam) {
82
                    $names[$parameter->name] = new AssistedResourceParam($resourceParam);
83
                    continue;
84
                }
85
            }
86
87
            $refWebContext = $parameter->getAttributes(AbstractWebContextParam::class, ReflectionAttribute::IS_INSTANCEOF);
88
            if ($refWebContext) {
89
                $webParam = $refWebContext[0]->newInstance();
90
                $default = $this->getDefault($parameter);
91
                $param = new AssistedWebContextParam($webParam, $default);
92
                $names[$parameter->name] = $param;
93
                continue;
94
            }
95
96
            // #[Input]
97
            $inputAttribute = $parameter->getAttributes(Input::class, ReflectionAttribute::IS_INSTANCEOF);
98
            if ($inputAttribute) {
99
                $names[$parameter->name] = new InputParam($this->inputQuery, $parameter);
100
                continue;
101
            }
102
103
            // #[InputFile]
104
            $inputFileAttributes = $parameter->getAttributes(InputFile::class, ReflectionAttribute::IS_INSTANCEOF);
105
            if ($inputFileAttributes) {
106
                $this->setInputFileParam($parameter, $inputFileAttributes, $names);
107
                continue;
108
            }
109
110
            $valueParams[$parameter->name] = $parameter;
111
        }
112
113
        $names = $this->getNames($names, $valueParams);
114
115
        return $names;
116
    }
117
118
    /**
119
     * @param ObjectList $annotations
120
     *
121
     * @return ParamMap
122
     */
123
    private function getAssistedNames(array $annotations): array
124
    {
125
        $names = [];
126
        foreach ($annotations as $annotation) {
127
            if ($annotation instanceof ResourceParam) {
128
                $names[$annotation->param] = new AssistedResourceParam($annotation);
129
            }
130
131
            if (! ($annotation instanceof Assisted)) {
132
                continue;
133
            }
134
135
            // @codeCoverageIgnoreStart
136
            $names = $this->setAssistedAnnotation($names, $annotation); // BC for annotation
137
            // @codeCoverageIgnoreEnd
138
        }
139
140
        return $names;
141
    }
142
143
    /**
144
     * @param ObjectList $annotations
145
     *
146
     * @return WebContextParamMap
147
     *
148
     * @codeCoverageIgnore BC for annotation
149
     * @psalm-suppress MixedReturnTypeCoercion
150
     * @psalm-suppress MixedArrayOffset
151
     * @psalm-suppress UndefinedPropertyFetch
152
     */
153
    private function getWebContext(array $annotations): array
154
    {
155
        $webcontext = [];
156
        foreach ($annotations as $annotation) {
157
            if (! ($annotation instanceof AbstractWebContextParam)) {
158
                continue;
159
            }
160
161
            $webcontext[$annotation->param] = $annotation;
162
        }
163
164
        return $webcontext;
165
    }
166
167
    /**
168
     * @param ParamMap $names
169
     *
170
     * @return ParamMap
171
     *
172
     * @codeCoverageIgnore BC for annotation
173
     * @psalm-suppress MixedReturnTypeCoercion
174
     * @psalm-suppress MixedArrayOffset
175
     * @psalm-suppress MixedAssignment
176
     * @psalm-suppress UndefinedPropertyFetch
177
     */
178
    private function setAssistedAnnotation(array $names, Assisted $assisted): array
179
    {
180
        foreach ($assisted->values as $assistedParam) {
181
            $names[$assistedParam] = new AssistedParam();
182
        }
183
184
        return $names;
185
    }
186
187
    /**
188
     * @param list<ReflectionParameter> $parameters
189
     * @param ParamMap                  $assistedNames
190
     * @param WebContextParamMap        $webcontext
191
     *
192
     * @return (AssistedWebContextParam|ParamInterface)[]
193
     * @psalm-return array<string, AssistedWebContextParam|ParamInterface>
194
     *
195
     * @psalm-suppress InvalidArgument
196
     */
197
    private function addNamedParams(array $parameters, array $assistedNames, array $webcontext): array
198
    {
199
        $names = [];
200
        foreach ($parameters as $parameter) {
201
            $name = $parameter->name;
202
            if (isset($assistedNames[$name])) {
203
                $names[$name] = $assistedNames[$parameter->name];
204
205
                continue;
206
            }
207
208
            if (isset($webcontext[$name])) {
209
                $default = $this->getDefault($parameter);
210
                $names[$name] = new AssistedWebContextParam($webcontext[$name], $default);
211
212
                continue;
213
            }
214
215
            $names[$name] = $this->getParam($parameter);
216
        }
217
218
        return $names;
219
    }
220
221
    /**
222
     * @param array<ReflectionAttribute<InputFile>> $inputFileAttributes
223
     * @param ParamMap                              $names
224
     */
225
    private function setInputFileParam(ReflectionParameter $parameter, array $inputFileAttributes, array &$names): void
226
    {
227
        $type = $parameter->getType();
228
        $isArray = $type instanceof ReflectionNamedType && $type->isBuiltin() && $type->getName() === 'array';
229
        if ($isArray) {
230
            $names[$parameter->name] = new InputFormsParam($this->factory, $parameter, $inputFileAttributes);
231
232
            return;
233
        }
234
235
        $names[$parameter->name] = new InputFormParam($this->factory, $parameter, $inputFileAttributes);
236
    }
237
238
    /** @psalm-return DefaultParam<mixed>|NoDefaultParam */
239
    private function getDefault(ReflectionParameter $parameter): DefaultParam|NoDefaultParam
240
    {
241
        return $parameter->isDefaultValueAvailable() === true ? new DefaultParam($parameter->getDefaultValue()) : new NoDefaultParam();
242
    }
243
244
    /**
245
     * @param ParamMap               $names
246
     * @param ReflectionParameterMap $valueParams
247
     *
248
     * @return ParamMap
249
     */
250
    private function getNames(array $names, array $valueParams): array
251
    {
252
        // if there is more than single attributes
253
        if ($names) {
254
            foreach ($valueParams as $paramName => $valueParam) {
255
                $names[$paramName] = $this->getParam($valueParam);
256
            }
257
        }
258
259
        return $names;
260
    }
261
262
    /**
263
     * @return ClassParam|OptionalParam|RequiredParam
264
     * @psalm-return ClassParam|OptionalParam<mixed>|RequiredParam
265
     */
266
    private function getParam(ReflectionParameter $parameter): ParamInterface
267
    {
268
        $type = $parameter->getType();
269
        if ($type instanceof ReflectionNamedType && ! $type->isBuiltin()) {
270
            return new ClassParam($type, $parameter);
271
        }
272
273
        return $parameter->isDefaultValueAvailable() === true ? new OptionalParam($parameter->getDefaultValue()) : new RequiredParam();
274
    }
275
}
276