Issues (141)

src/NamedParamMetas.php (1 issue)

Labels
Severity
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\InputQuery\Attribute\Input;
12
use Ray\InputQuery\Attribute\InputFile;
13
use Ray\InputQuery\FileUploadFactoryInterface;
14
use Ray\InputQuery\InputQueryInterface;
15
use Ray\WebContextParam\Annotation\AbstractWebContextParam;
16
use ReflectionAttribute;
17
use ReflectionNamedType;
18
use ReflectionParameter;
19
20
/**
21
 * @psalm-import-type ParamMap from Types
22
 * @psalm-import-type WebContextParamMap from Types
23
 * @psalm-import-type ReflectionParameterMap from Types
24
 * @psalm-import-type ObjectList from Types
25
 */
26
final readonly class NamedParamMetas implements NamedParamMetasInterface
0 ignored issues
show
A parse error occurred: Syntax error, unexpected T_READONLY, expecting T_CLASS on line 26 at column 6
Loading history...
27
{
28
    /** @param InputQueryInterface<object> $inputQuery */
29
    public function __construct(
30
        private InputQueryInterface $inputQuery,
31
        private FileUploadFactoryInterface $factory,
32
    ) {
33
    }
34
35
    /**
36
     * {@inheritDoc}
37
     */
38
    #[Override]
39
    public function __invoke(callable $callable): array
40
    {
41
        // callable is [object, string] but native type doesn't allow array access
42
        /** @psalm-suppress InvalidArrayAccess, MixedArgument */
43
        /** @var array{0:object, 1:string} $callable */
44
        $method = new ReflectionMethod($callable[0], $callable[1]); // @phpstan-ignore-line
45
46
        return $this->getAttributeParamMetas($method);
47
    }
48
49
    /**
50
     * @return ParamMap
51
     *
52
     * @psalm-suppress TooManyTemplateParams $refAttribute
53
     * @psalm-suppress PossiblyInvalidArrayAssignment
54
     */
55
    private function getAttributeParamMetas(ReflectionMethod $method): array
56
    {
57
        $parameters = $method->getParameters();
58
        $names = $valueParams = [];
59
60
        // Check method-level ResourceParam attributes
61
        $methodResourceParams = $method->getAttributes(ResourceParam::class);
62
        foreach ($methodResourceParams as $methodAttr) {
63
            $resourceParam = $methodAttr->newInstance();
64
            $names[$resourceParam->param] = new AssistedResourceParam($resourceParam);
65
        }
66
67
        foreach ($parameters as $parameter) {
68
            // Skip if already set by method-level attribute
69
            if (isset($names[$parameter->name])) {
70
                continue;
71
            }
72
73
            $refAttribute = $parameter->getAttributes(RequestParamInterface::class, ReflectionAttribute::IS_INSTANCEOF);
74
            if ($refAttribute) {
75
                /** @var ?ResourceParam $resourceParam */
76
                $resourceParam = $refAttribute[0]->newInstance();
77
                if ($resourceParam instanceof ResourceParam) {
78
                    $names[$parameter->name] = new AssistedResourceParam($resourceParam);
79
                    continue;
80
                }
81
            }
82
83
            $refWebContext = $parameter->getAttributes(AbstractWebContextParam::class, ReflectionAttribute::IS_INSTANCEOF);
84
            if ($refWebContext) {
85
                $webParam = $refWebContext[0]->newInstance();
86
                $default = $this->getDefault($parameter);
87
                $param = new AssistedWebContextParam($webParam, $default);
88
                $names[$parameter->name] = $param;
89
                continue;
90
            }
91
92
            // #[Input]
93
            $inputAttribute = $parameter->getAttributes(Input::class, ReflectionAttribute::IS_INSTANCEOF);
94
            if ($inputAttribute) {
95
                $names[$parameter->name] = new InputParam($this->inputQuery, $parameter);
96
                continue;
97
            }
98
99
            // #[InputFile]
100
            $inputFileAttributes = $parameter->getAttributes(InputFile::class, ReflectionAttribute::IS_INSTANCEOF);
101
            if ($inputFileAttributes) {
102
                $this->setInputFileParam($parameter, $inputFileAttributes, $names);
103
                continue;
104
            }
105
106
            $valueParams[$parameter->name] = $parameter;
107
        }
108
109
        $names = $this->getNames($names, $valueParams);
110
111
        return $names;
112
    }
113
114
    /**
115
     * @param array<ReflectionAttribute<InputFile>> $inputFileAttributes
116
     * @param ParamMap                              $names
117
     */
118
    private function setInputFileParam(ReflectionParameter $parameter, array $inputFileAttributes, array &$names): void
119
    {
120
        $type = $parameter->getType();
121
        $isArray = $type instanceof ReflectionNamedType && $type->isBuiltin() && $type->getName() === 'array';
122
        if ($isArray) {
123
            $names[$parameter->name] = new InputFormsParam($this->factory, $parameter, $inputFileAttributes);
124
125
            return;
126
        }
127
128
        $names[$parameter->name] = new InputFormParam($this->factory, $parameter, $inputFileAttributes);
129
    }
130
131
    /** @psalm-return DefaultParam<mixed>|NoDefaultParam */
132
    private function getDefault(ReflectionParameter $parameter): DefaultParam|NoDefaultParam
133
    {
134
        return $parameter->isDefaultValueAvailable() === true ? new DefaultParam($parameter->getDefaultValue()) : new NoDefaultParam();
135
    }
136
137
    /**
138
     * @param ParamMap               $names
139
     * @param ReflectionParameterMap $valueParams
140
     *
141
     * @return ParamMap
142
     */
143
    private function getNames(array $names, array $valueParams): array
144
    {
145
        foreach ($valueParams as $paramName => $valueParam) {
146
            $names[$paramName] = $this->getParam($valueParam);
147
        }
148
149
        return $names;
150
    }
151
152
    /**
153
     * @return ClassParam|OptionalParam|RequiredParam
154
     * @psalm-return ClassParam|OptionalParam<mixed>|RequiredParam
155
     */
156
    private function getParam(ReflectionParameter $parameter): ParamInterface
157
    {
158
        $type = $parameter->getType();
159
        if ($type instanceof ReflectionNamedType && ! $type->isBuiltin()) {
160
            return new ClassParam($type, $parameter);
161
        }
162
163
        return $parameter->isDefaultValueAvailable() === true ? new OptionalParam($parameter->getDefaultValue()) : new RequiredParam();
164
    }
165
}
166