Issues (150)

src/NamedParamMetas.php (6 issues)

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
final class NamedParamMetas implements NamedParamMetasInterface
22
{
23
    /** @param InputQueryInterface<object> $inputQuery */
24
    public function __construct(
25
        private readonly InputQueryInterface $inputQuery,
26
        private readonly FileUploadFactoryInterface $factory,
27
    ) {
28
    }
29
30
    /**
31
     * {@inheritDoc}
32
     */
33
    #[Override]
34
    public function __invoke(callable $callable): array
35
    {
36
        /** @var array{0:object, 1:string} $callable */
37
        $method = new ReflectionMethod($callable[0], $callable[1]);
38
        $paramMetas = $this->getAttributeParamMetas($method);
39
40
        if (! $paramMetas) {
41
            $paramMetas = $this->getAnnotationParamMetas($method);
42
        }
43
44
        return $paramMetas;
45
    }
46
47
    /** @return array<string, AssistedWebContextParam|ParamInterface> */
48
    private function getAnnotationParamMetas(ReflectionMethod $method): array
49
    {
50
        $parameters = $method->getParameters();
51
        $annotations = $method->getAnnotations();
52
        $assistedNames = $this->getAssistedNames($annotations);
53
        $webContext = $this->getWebContext($annotations);
54
55
        return $this->addNamedParams($parameters, $assistedNames, $webContext);
56
    }
57
58
    /**
59
     * @return array<string, ParamInterface>
60
     *
61
     * @psalm-suppress TooManyTemplateParams $refAttribute
62
     */
63
    private function getAttributeParamMetas(ReflectionMethod $method): array
64
    {
65
        $parameters = $method->getParameters();
66
        $names = $valueParams = [];
67
        foreach ($parameters as $parameter) {
68
            $refAttribute = $parameter->getAttributes(RequestParamInterface::class, ReflectionAttribute::IS_INSTANCEOF);
69
            if ($refAttribute) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $refAttribute 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...
70
                /** @var ?ResourceParam $resourceParam */
71
                $resourceParam = $refAttribute[0]->newInstance();
72
                if ($resourceParam instanceof ResourceParam) {
73
                    $names[$parameter->name] = new AssistedResourceParam($resourceParam);
74
                    continue;
75
                }
76
            }
77
78
            $refWebContext = $parameter->getAttributes(AbstractWebContextParam::class, ReflectionAttribute::IS_INSTANCEOF);
79
            if ($refWebContext) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $refWebContext 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...
80
                $webParam = $refWebContext[0]->newInstance();
81
                $default = $this->getDefault($parameter);
82
                $param = new AssistedWebContextParam($webParam, $default);
83
                $names[$parameter->name] = $param;
84
                continue;
85
            }
86
87
            // #[Input]
88
            $inputAttribute = $parameter->getAttributes(Input::class, ReflectionAttribute::IS_INSTANCEOF);
89
            if ($inputAttribute) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inputAttribute 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...
90
                $names[$parameter->name] = new InputParam($this->inputQuery, $parameter);
91
                continue;
92
            }
93
94
            // #[InputFile]
95
            $inputFileAttributes = $parameter->getAttributes(InputFile::class, ReflectionAttribute::IS_INSTANCEOF);
96
            if ($inputFileAttributes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inputFileAttributes 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...
97
                $this->setInputFileParam($parameter, $inputFileAttributes, $names);
98
                continue;
99
            }
100
101
            $valueParams[$parameter->name] = $parameter;
102
        }
103
104
        $names = $this->getNames($names, $valueParams);
105
106
        return $names;
107
    }
108
109
    /**
110
     * @param array<Assisted|object|ResourceParam> $annotations
111
     *
112
     * @return array<string, ParamInterface>
113
     */
114
    private function getAssistedNames(array $annotations): array
115
    {
116
        $names = [];
117
        foreach ($annotations as $annotation) {
118
            if ($annotation instanceof ResourceParam) {
119
                $names[$annotation->param] = new AssistedResourceParam($annotation);
120
            }
121
122
            if (! ($annotation instanceof Assisted)) {
123
                continue;
124
            }
125
126
            // @codeCoverageIgnoreStart
127
            $names = $this->setAssistedAnnotation($names, $annotation); // BC for annotation
128
            // @codeCoverageIgnoreEnd
129
        }
130
131
        return $names;
132
    }
133
134
    /**
135
     * @param array<object> $annotations
136
     *
137
     * @return array<string, AbstractWebContextParam>
138
     */
139
    private function getWebContext(array $annotations): array
140
    {
141
        $webcontext = [];
142
        foreach ($annotations as $annotation) {
143
            if (! ($annotation instanceof AbstractWebContextParam)) {
144
                continue;
145
            }
146
147
            $webcontext[$annotation->param] = $annotation;
148
        }
149
150
        return $webcontext;
151
    }
152
153
    /**
154
     * @param array<string, ParamInterface> $names
155
     *
156
     * @return array<string, ParamInterface>
157
     *
158
     * @codeCoverageIgnore BC for annotation
159
     */
160
    private function setAssistedAnnotation(array $names, Assisted $assisted): array
161
    {
162
        foreach ($assisted->values as $assistedParam) {
163
            $names[$assistedParam] = new AssistedParam();
164
        }
165
166
        return $names;
167
    }
168
169
    /**
170
     * @param ReflectionParameter[]                  $parameters
171
     * @param array<string, ParamInterface>          $assistedNames
172
     * @param array<string, AbstractWebContextParam> $webcontext
173
     *
174
     * @return (AssistedWebContextParam|ParamInterface)[]
175
     * @psalm-return array<string, AssistedWebContextParam|ParamInterface>
176
     */
177
    private function addNamedParams(array $parameters, array $assistedNames, array $webcontext): array
178
    {
179
        $names = [];
180
        foreach ($parameters as $parameter) {
181
            $name = $parameter->name;
182
            if (isset($assistedNames[$name])) {
183
                $names[$name] = $assistedNames[$parameter->name];
184
185
                continue;
186
            }
187
188
            if (isset($webcontext[$name])) {
189
                $default = $this->getDefault($parameter);
190
                $names[$name] = new AssistedWebContextParam($webcontext[$name], $default);
191
192
                continue;
193
            }
194
195
            $names[$name] = $this->getParam($parameter);
196
        }
197
198
        return $names;
199
    }
200
201
    /**
202
     * @param array<ReflectionAttribute<InputFile>> $inputFileAttributes
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<ReflectionAttribute<InputFile>> at position 2 could not be parsed: Expected '>' at position 2, but found 'ReflectionAttribute'.
Loading history...
203
     * @param array<string, ParamInterface>         $names
204
     */
205
    private function setInputFileParam(ReflectionParameter $parameter, array $inputFileAttributes, array &$names): void
206
    {
207
        $type = $parameter->getType();
208
        $isArray = $type instanceof ReflectionNamedType && $type->isBuiltin() && $type->getName() === 'array';
209
        if ($isArray) {
210
            $names[$parameter->name] = new InputFormsParam($this->factory, $parameter, $inputFileAttributes);
211
212
            return;
213
        }
214
215
        $names[$parameter->name] = new InputFormParam($this->factory, $parameter, $inputFileAttributes);
216
    }
217
218
    /** @psalm-return DefaultParam<mixed>|NoDefaultParam */
219
    private function getDefault(ReflectionParameter $parameter): DefaultParam|NoDefaultParam
220
    {
221
        return $parameter->isDefaultValueAvailable() === true ? new DefaultParam($parameter->getDefaultValue()) : new NoDefaultParam();
222
    }
223
224
    /**
225
     * @param array<string, ParamInterface>      $names
226
     * @param array<string, ReflectionParameter> $valueParams
227
     *
228
     * @return array<string, ParamInterface>
229
     */
230
    private function getNames(array $names, array $valueParams): array
231
    {
232
        // if there is more than single attributes
233
        if ($names) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $names of type array<string,BEAR\Resource\ParamInterface> 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...
234
            foreach ($valueParams as $paramName => $valueParam) {
235
                $names[$paramName] = $this->getParam($valueParam);
236
            }
237
        }
238
239
        return $names;
240
    }
241
242
    /**
243
     * @return ClassParam|OptionalParam|RequiredParam
244
     * @psalm-return ClassParam|OptionalParam<mixed>|RequiredParam
245
     */
246
    private function getParam(ReflectionParameter $parameter): ParamInterface
247
    {
248
        $type = $parameter->getType();
249
        if ($type instanceof ReflectionNamedType && ! $type->isBuiltin()) {
250
            return new ClassParam($type, $parameter);
251
        }
252
253
        return $parameter->isDefaultValueAvailable() === true ? new OptionalParam($parameter->getDefaultValue()) : new RequiredParam();
254
    }
255
}
256